diff --git a/.betterer.results b/.betterer.results index 19cfd61c989..f20d99d74fb 100644 --- a/.betterer.results +++ b/.betterer.results @@ -523,7 +523,8 @@ exports[`better eslint`] = { ], "packages/grafana-data/src/types/logs.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], - [0, 0, 0, "Do not use any type assertions.", "1"] + [0, 0, 0, "Do not use any type assertions.", "1"], + [0, 0, 0, "Do not use any type assertions.", "2"] ], "packages/grafana-data/src/types/logsVolume.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] @@ -1688,97 +1689,6 @@ exports[`better eslint`] = { "packages/grafana-ui/src/utils/useAsyncDependency.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], - "packages/jaeger-ui-components/src/ScrollManager.tsx:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"], - [0, 0, 0, "Do not use any type assertions.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"] - ], - "packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/Scrubber.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"] - ], - "packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/render-into-canvas.tsx:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], - "packages/jaeger-ui-components/src/TraceTimelineViewer/ListView/index.tsx:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Do not use any type assertions.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"], - [0, 0, 0, "Do not use any type assertions.", "4"] - ], - "packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBar.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], - "packages/jaeger-ui-components/src/TraceTimelineViewer/TimelineHeaderRow/TimelineViewingLayer.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], - "packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.tsx:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], - "packages/jaeger-ui-components/src/TraceTimelineViewer/utils.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], - "packages/jaeger-ui-components/src/common/BreakableText.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], - "packages/jaeger-ui-components/src/common/UiFindInput.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"] - ], - "packages/jaeger-ui-components/src/constants/index.tsx:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"], - [0, 0, 0, "Do not use any type assertions.", "1"], - [0, 0, 0, "Do not use any type assertions.", "2"], - [0, 0, 0, "Do not use any type assertions.", "3"], - [0, 0, 0, "Do not use any type assertions.", "4"] - ], - "packages/jaeger-ui-components/src/constants/tag-keys.tsx:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"], - [0, 0, 0, "Do not use any type assertions.", "1"], - [0, 0, 0, "Do not use any type assertions.", "2"] - ], - "packages/jaeger-ui-components/src/keyboard-shortcuts.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"] - ], - "packages/jaeger-ui-components/src/model/link-patterns.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"], - [0, 0, 0, "Unexpected any. Specify a different type.", "4"], - [0, 0, 0, "Unexpected any. Specify a different type.", "5"], - [0, 0, 0, "Unexpected any. Specify a different type.", "6"], - [0, 0, 0, "Unexpected any. Specify a different type.", "7"], - [0, 0, 0, "Unexpected any. Specify a different type.", "8"], - [0, 0, 0, "Unexpected any. Specify a different type.", "9"], - [0, 0, 0, "Do not use any type assertions.", "10"], - [0, 0, 0, "Unexpected any. Specify a different type.", "11"], - [0, 0, 0, "Do not use any type assertions.", "12"], - [0, 0, 0, "Unexpected any. Specify a different type.", "13"] - ], - "packages/jaeger-ui-components/src/model/transform-trace-data.tsx:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"], - [0, 0, 0, "Do not use any type assertions.", "1"], - [0, 0, 0, "Do not use any type assertions.", "2"] - ], - "packages/jaeger-ui-components/src/types/api-error.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], - "packages/jaeger-ui-components/src/types/links.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], - "packages/jaeger-ui-components/src/types/trace.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], - "packages/jaeger-ui-components/src/utils/DraggableManager/types.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], - "packages/jaeger-ui-components/src/utils/date.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], "plugins-bundled/internal/input-datasource/src/InputDatasource.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"] @@ -3196,8 +3106,10 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "6"], [0, 0, 0, "Unexpected any. Specify a different type.", "7"], [0, 0, 0, "Do not use any type assertions.", "8"], - [0, 0, 0, "Do not use any type assertions.", "9"], - [0, 0, 0, "Unexpected any. Specify a different type.", "10"] + [0, 0, 0, "Unexpected any. Specify a different type.", "9"], + [0, 0, 0, "Do not use any type assertions.", "10"], + [0, 0, 0, "Do not use any type assertions.", "11"], + [0, 0, 0, "Unexpected any. Specify a different type.", "12"] ], "public/app/features/dashboard/components/DashboardPrompt/DashboardPrompt.test.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], @@ -3882,6 +3794,63 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "4"], [0, 0, 0, "Do not use any type assertions.", "5"] ], + "public/app/features/explore/TraceView/components/ScrollManager.tsx:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"], + [0, 0, 0, "Do not use any type assertions.", "1"], + [0, 0, 0, "Unexpected any. Specify a different type.", "2"] + ], + "public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/Scrubber.tsx:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"], + [0, 0, 0, "Unexpected any. Specify a different type.", "1"], + [0, 0, 0, "Unexpected any. Specify a different type.", "2"] + ], + "public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/render-into-canvas.tsx:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"] + ], + "public/app/features/explore/TraceView/components/TraceTimelineViewer/ListView/index.tsx:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"] + ], + "public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBar.tsx:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"] + ], + "public/app/features/explore/TraceView/components/TraceTimelineViewer/utils.tsx:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"] + ], + "public/app/features/explore/TraceView/components/common/BreakableText.tsx:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"] + ], + "public/app/features/explore/TraceView/components/constants/index.tsx:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"] + ], + "public/app/features/explore/TraceView/components/demo/trace-generators.ts:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"] + ], + "public/app/features/explore/TraceView/components/model/link-patterns.test.ts:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"], + [0, 0, 0, "Unexpected any. Specify a different type.", "1"] + ], + "public/app/features/explore/TraceView/components/model/link-patterns.tsx:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"], + [0, 0, 0, "Unexpected any. Specify a different type.", "1"], + [0, 0, 0, "Unexpected any. Specify a different type.", "2"], + [0, 0, 0, "Unexpected any. Specify a different type.", "3"], + [0, 0, 0, "Unexpected any. Specify a different type.", "4"], + [0, 0, 0, "Unexpected any. Specify a different type.", "5"], + [0, 0, 0, "Unexpected any. Specify a different type.", "6"], + [0, 0, 0, "Unexpected any. Specify a different type.", "7"], + [0, 0, 0, "Unexpected any. Specify a different type.", "8"], + [0, 0, 0, "Unexpected any. Specify a different type.", "9"], + [0, 0, 0, "Do not use any type assertions.", "10"], + [0, 0, 0, "Unexpected any. Specify a different type.", "11"], + [0, 0, 0, "Do not use any type assertions.", "12"], + [0, 0, 0, "Unexpected any. Specify a different type.", "13"] + ], + "public/app/features/explore/TraceView/components/model/transform-trace-data.tsx:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"] + ], + "public/app/features/explore/TraceView/components/types/trace.ts:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"] + ], "public/app/features/explore/TraceView/createSpanLink.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"] @@ -4021,9 +3990,6 @@ exports[`better eslint`] = { "public/app/features/library-panels/components/LibraryPanelsView/actions.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], - "public/app/features/library-panels/types.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], "public/app/features/library-panels/utils.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] @@ -4124,6 +4090,9 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "4"], [0, 0, 0, "Unexpected any. Specify a different type.", "5"] ], + "public/app/features/manage-dashboards/components/ImportDashboardLibraryPanelsList.tsx:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"] + ], "public/app/features/manage-dashboards/components/SnapshotListTable.test.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], @@ -4585,8 +4554,7 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], "public/app/features/search/utils.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"], - [0, 0, 0, "Do not use any type assertions.", "1"] + [0, 0, 0, "Do not use any type assertions.", "0"] ], "public/app/features/serviceaccounts/ServiceAccountPage.test.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], @@ -6098,9 +6066,10 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "1"], [0, 0, 0, "Unexpected any. Specify a different type.", "2"], [0, 0, 0, "Unexpected any. Specify a different type.", "3"], - [0, 0, 0, "Unexpected any. Specify a different type.", "4"], + [0, 0, 0, "Do not use any type assertions.", "4"], [0, 0, 0, "Unexpected any. Specify a different type.", "5"], - [0, 0, 0, "Unexpected any. Specify a different type.", "6"] + [0, 0, 0, "Unexpected any. Specify a different type.", "6"], + [0, 0, 0, "Unexpected any. Specify a different type.", "7"] ], "public/app/plugins/datasource/loki/getDerivedFields.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], diff --git a/.drone.star b/.drone.star index f5994191a0c..8c2a3c03767 100644 --- a/.drone.star +++ b/.drone.star @@ -3,54 +3,55 @@ # 2. Login to drone and export the env variables (token and server) shown here: https://drone.grafana.net/account # 3. Run `make drone` # More information about this process here: https://github.com/grafana/deployment_tools/blob/master/docs/infrastructure/drone/signing.md +""" +This module returns a Drone configuration including pipelines and secrets. +""" -load('scripts/drone/events/pr.star', 'pr_pipelines') -load('scripts/drone/events/main.star', 'main_pipelines') -load('scripts/drone/pipelines/docs.star', 'docs_pipelines') +load("scripts/drone/events/pr.star", "pr_pipelines") +load("scripts/drone/events/main.star", "main_pipelines") load( - 'scripts/drone/events/release.star', - 'oss_pipelines', - 'enterprise_pipelines', - 'enterprise2_pipelines', - 'publish_artifacts_pipelines', - 'publish_npm_pipelines', - 'publish_packages_pipeline', - 'artifacts_page_pipeline', + "scripts/drone/events/release.star", + "artifacts_page_pipeline", + "enterprise2_pipelines", + "enterprise_pipelines", + "oss_pipelines", + "publish_artifacts_pipelines", + "publish_npm_pipelines", + "publish_packages_pipeline", ) load( - 'scripts/drone/pipelines/publish_images.star', - 'publish_image_pipelines_public', - 'publish_image_pipelines_security', + "scripts/drone/pipelines/publish_images.star", + "publish_image_pipelines_public", + "publish_image_pipelines_security", ) -load('scripts/drone/pipelines/github.star', 'publish_github_pipeline') -load('scripts/drone/pipelines/aws_marketplace.star', 'publish_aws_marketplace_pipeline') -load('scripts/drone/version.star', 'version_branch_pipelines') -load('scripts/drone/events/cron.star', 'cronjobs') -load('scripts/drone/vault.star', 'secrets') +load("scripts/drone/pipelines/github.star", "publish_github_pipeline") +load("scripts/drone/pipelines/aws_marketplace.star", "publish_aws_marketplace_pipeline") +load("scripts/drone/version.star", "version_branch_pipelines") +load("scripts/drone/events/cron.star", "cronjobs") +load("scripts/drone/vault.star", "secrets") - -def main(ctx): +def main(_ctx): return ( - pr_pipelines() - + main_pipelines() - + oss_pipelines() - + enterprise_pipelines() - + enterprise2_pipelines() - + enterprise2_pipelines( - prefix='custom-', - trigger={'event': ['custom']}, - ) - + publish_image_pipelines_public() - + publish_image_pipelines_security() - + publish_github_pipeline('public') - + publish_github_pipeline('security') - + publish_aws_marketplace_pipeline('public') - + publish_artifacts_pipelines('security') - + publish_artifacts_pipelines('public') - + publish_npm_pipelines() - + publish_packages_pipeline() - + artifacts_page_pipeline() - + version_branch_pipelines() - + cronjobs() - + secrets() + pr_pipelines() + + main_pipelines() + + oss_pipelines() + + enterprise_pipelines() + + enterprise2_pipelines() + + enterprise2_pipelines( + prefix = "custom-", + trigger = {"event": ["custom"]}, + ) + + publish_image_pipelines_public() + + publish_image_pipelines_security() + + publish_github_pipeline("public") + + publish_github_pipeline("security") + + publish_aws_marketplace_pipeline("public") + + publish_artifacts_pipelines("security") + + publish_artifacts_pipelines("public") + + publish_npm_pipelines() + + publish_packages_pipeline() + + artifacts_page_pipeline() + + version_branch_pipelines() + + cronjobs() + + secrets() ) diff --git a/.drone.yml b/.drone.yml index e787f4007f8..00584d6a234 100644 --- a/.drone.yml +++ b/.drone.yml @@ -63,6 +63,61 @@ environment: image_pull_secrets: - dockerconfigjson kind: pipeline +name: pr-verify-starlark +node: + type: no-parallel +platform: + arch: amd64 + os: linux +services: [] +steps: +- commands: + - echo $DRONE_RUNNER_NAME + image: alpine:3.15.6 + name: identify-runner +- commands: + - mkdir -p bin + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.20/grabpl + - chmod +x bin/grabpl + image: byrnedo/alpine-curl:0.1.8 + name: grabpl +- commands: + - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd + depends_on: [] + environment: + CGO_ENABLED: 0 + image: golang:1.19.4 + name: compile-build-cmd +- commands: + - ./bin/build verify-starlark . + depends_on: + - compile-build-cmd + image: grafana/build-container:v1.7.1 + name: lint-starlark +trigger: + event: + - pull_request + paths: + exclude: + - docs/** + - '*.md' + include: + - scripts/drone/** + - .drone.star +type: docker +volumes: +- host: + path: /var/run/docker.sock + name: docker +--- +clone: + retries: 3 +depends_on: [] +environment: + EDITION: oss +image_pull_secrets: +- dockerconfigjson +kind: pipeline name: pr-test-frontend node: type: no-parallel @@ -84,13 +139,13 @@ steps: - commands: - yarn install --immutable depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: yarn-install - commands: - yarn betterer ci depends_on: - yarn-install - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: betterer-frontend - commands: - yarn run ci:test-frontend @@ -98,7 +153,7 @@ steps: - yarn-install environment: TEST_MAX_WORKERS: 50% - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: test-frontend trigger: event: @@ -141,7 +196,7 @@ steps: - commands: - yarn install --immutable depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: yarn-install - commands: - yarn run prettier:check @@ -152,7 +207,7 @@ steps: - yarn-install environment: TEST_MAX_WORKERS: 50% - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: lint-frontend trigger: event: @@ -206,7 +261,7 @@ steps: in output.' - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -214,25 +269,25 @@ steps: in output.' - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-jsonnet - commands: - make gen-go depends_on: - verify-gen-cue - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: wire-install - commands: - go test -short -covermode=atomic -timeout=5m ./pkg/... depends_on: - wire-install - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: test-backend - commands: - go test -run Integration -covermode=atomic -timeout=5m ./pkg/... depends_on: - wire-install - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: test-backend-integration trigger: event: @@ -286,7 +341,7 @@ steps: - commands: - make gen-go depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: wire-install - commands: - apt-get update && apt-get install make @@ -359,7 +414,7 @@ steps: in output.' - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -367,18 +422,18 @@ steps: in output.' - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-jsonnet - commands: - make gen-go depends_on: - verify-gen-cue - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: wire-install - commands: - yarn install --immutable depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: yarn-install - commands: - git clone "https://$${GITHUB_TOKEN}@github.com/grafana/grafana-enterprise.git" @@ -403,7 +458,7 @@ steps: from_secret: github_token_pr TEST_TAG: v0.0.0-test failure: ignore - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: trigger-test-release when: paths: @@ -430,7 +485,7 @@ steps: depends_on: - wire-install - compile-build-cmd - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-backend - commands: - ./bin/build build-frontend --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER} @@ -439,7 +494,7 @@ steps: - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-frontend - commands: - ./bin/build build-frontend-packages --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER} @@ -448,7 +503,7 @@ steps: - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-frontend-packages - commands: - ./bin/build build-plugins --jobs 8 --edition oss @@ -456,7 +511,7 @@ steps: - compile-build-cmd - yarn-install environment: null - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-plugins - commands: - . scripts/build/gpg-test-vars.sh && ./bin/build package --jobs 8 --edition oss @@ -467,7 +522,7 @@ steps: - build-frontend - build-frontend-packages environment: null - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: package - commands: - ./scripts/grafana-server/start-server @@ -480,7 +535,7 @@ steps: environment: ARCH: linux-amd64 PORT: 3001 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: grafana-server - commands: - apt-get install -y netcat @@ -583,7 +638,7 @@ steps: - build-frontend-packages environment: NODE_OPTIONS: --max_old_space_size=4096 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-storybook when: paths: @@ -594,7 +649,7 @@ steps: - cp dist/*.tar.gz* packaging/docker/ depends_on: - package - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: copy-packages-for-docker - commands: - yarn wait-on http://$HOST:$PORT @@ -696,7 +751,7 @@ steps: in output.' - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -704,13 +759,13 @@ steps: in output.' - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-jsonnet - commands: - make gen-go depends_on: - verify-gen-cue - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: wire-install - commands: - apt-get update @@ -726,7 +781,7 @@ steps: GRAFANA_TEST_DB: postgres PGPASSWORD: grafanatest POSTGRES_HOST: postgres - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: postgres-integration-tests - commands: - apt-get update @@ -742,7 +797,7 @@ steps: environment: GRAFANA_TEST_DB: mysql MYSQL_HOST: mysql - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: mysql-integration-tests trigger: event: @@ -800,7 +855,7 @@ steps: - commands: - yarn install --immutable depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: yarn-install - commands: - |- @@ -812,7 +867,7 @@ steps: wan" > words_to_ignore.txt - codespell -I words_to_ignore.txt docs/ - rm words_to_ignore.txt - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: codespell - commands: - yarn run prettier:checkDocs @@ -820,7 +875,7 @@ steps: - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: lint-docs - commands: - mkdir -p /hugo/content/docs/grafana @@ -870,7 +925,7 @@ steps: - ./bin/build shellcheck depends_on: - compile-build-cmd - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: shellcheck trigger: event: @@ -917,7 +972,7 @@ steps: - commands: - yarn install --immutable depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: yarn-install - commands: - |- @@ -929,7 +984,7 @@ steps: wan" > words_to_ignore.txt - codespell -I words_to_ignore.txt docs/ - rm words_to_ignore.txt - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: codespell - commands: - yarn run prettier:checkDocs @@ -937,7 +992,7 @@ steps: - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: lint-docs - commands: - mkdir -p /hugo/content/docs/grafana @@ -990,13 +1045,13 @@ steps: - commands: - yarn install --immutable depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: yarn-install - commands: - yarn betterer ci depends_on: - yarn-install - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: betterer-frontend - commands: - yarn run ci:test-frontend @@ -1004,7 +1059,7 @@ steps: - yarn-install environment: TEST_MAX_WORKERS: 50% - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: test-frontend trigger: branch: main @@ -1044,7 +1099,7 @@ steps: - commands: - yarn install --immutable depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: yarn-install - commands: - yarn run prettier:check @@ -1055,7 +1110,7 @@ steps: - yarn-install environment: TEST_MAX_WORKERS: 50% - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: lint-frontend trigger: branch: main @@ -1106,7 +1161,7 @@ steps: in output.' - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -1114,25 +1169,25 @@ steps: in output.' - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-jsonnet - commands: - make gen-go depends_on: - verify-gen-cue - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: wire-install - commands: - go test -short -covermode=atomic -timeout=5m ./pkg/... depends_on: - wire-install - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: test-backend - commands: - go test -run Integration -covermode=atomic -timeout=5m ./pkg/... depends_on: - wire-install - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: test-backend-integration trigger: branch: main @@ -1179,7 +1234,7 @@ steps: - commands: - make gen-go depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: wire-install - commands: - apt-get update && apt-get install make @@ -1251,7 +1306,7 @@ steps: in output.' - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -1259,25 +1314,25 @@ steps: in output.' - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-jsonnet - commands: - make gen-go depends_on: - verify-gen-cue - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: wire-install - commands: - yarn install --immutable depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: yarn-install - commands: - ./bin/build build-backend --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER} depends_on: - wire-install - compile-build-cmd - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-backend - commands: - ./bin/build build-frontend --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER} @@ -1286,7 +1341,7 @@ steps: - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-frontend - commands: - ./bin/build build-frontend-packages --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER} @@ -1295,7 +1350,7 @@ steps: - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-frontend-packages - commands: - ./bin/build build-plugins --jobs 8 --edition oss @@ -1305,7 +1360,7 @@ steps: environment: GRAFANA_API_KEY: from_secret: grafana_api_key - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-plugins - commands: - ./bin/build package --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER} --sign @@ -1323,7 +1378,7 @@ steps: from_secret: packages_gpg_public_key GRAFANA_API_KEY: from_secret: grafana_api_key - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: package - commands: - ./scripts/grafana-server/start-server @@ -1336,7 +1391,7 @@ steps: environment: ARCH: linux-amd64 PORT: 3001 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: grafana-server - commands: - apt-get install -y netcat @@ -1439,7 +1494,7 @@ steps: - build-frontend-packages environment: NODE_OPTIONS: --max_old_space_size=4096 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-storybook when: paths: @@ -1450,7 +1505,7 @@ steps: - cp dist/*.tar.gz* packaging/docker/ depends_on: - package - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: copy-packages-for-docker - commands: - yarn wait-on http://$HOST:$PORT @@ -1494,7 +1549,7 @@ steps: GRAFANA_MISC_STATS_API_KEY: from_secret: grafana_misc_stats_api_key failure: ignore - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: publish-frontend-metrics when: repo: @@ -1575,7 +1630,7 @@ steps: environment: NPM_TOKEN: from_secret: npm_token - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: release-canary-npm-packages when: repo: @@ -1686,7 +1741,7 @@ steps: in output.' - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -1694,13 +1749,13 @@ steps: in output.' - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-jsonnet - commands: - make gen-go depends_on: - verify-gen-cue - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: wire-install - commands: - apt-get update @@ -1716,7 +1771,7 @@ steps: GRAFANA_TEST_DB: postgres PGPASSWORD: grafanatest POSTGRES_HOST: postgres - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: postgres-integration-tests - commands: - apt-get update @@ -1732,7 +1787,7 @@ steps: environment: GRAFANA_TEST_DB: mysql MYSQL_HOST: mysql - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: mysql-integration-tests trigger: branch: main @@ -1970,18 +2025,18 @@ steps: in output.' - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-cue - commands: - make gen-go depends_on: - verify-gen-cue - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: wire-install - commands: - yarn install --immutable depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: yarn-install - commands: - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd @@ -1995,7 +2050,7 @@ steps: depends_on: - wire-install - compile-build-cmd - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-backend - commands: - ./bin/build build-frontend --jobs 8 --edition oss ${DRONE_TAG} @@ -2004,7 +2059,7 @@ steps: - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-frontend - commands: - ./bin/build build-frontend-packages --jobs 8 --edition oss ${DRONE_TAG} @@ -2013,7 +2068,7 @@ steps: - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-frontend-packages - commands: - ./bin/build build-plugins --jobs 8 --edition oss @@ -2023,7 +2078,7 @@ steps: environment: GRAFANA_API_KEY: from_secret: grafana_api_key - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-plugins - commands: - ./bin/build package --jobs 8 --edition oss --sign ${DRONE_TAG} @@ -2041,14 +2096,14 @@ steps: from_secret: packages_gpg_public_key GRAFANA_API_KEY: from_secret: grafana_api_key - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: package - commands: - ls dist/*.tar.gz* - cp dist/*.tar.gz* packaging/docker/ depends_on: - package - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: copy-packages-for-docker - commands: - ./bin/build build-docker --edition oss --shouldSave @@ -2087,7 +2142,7 @@ steps: environment: ARCH: linux-amd64 PORT: 3001 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: grafana-server - commands: - apt-get install -y netcat @@ -2164,7 +2219,7 @@ steps: - build-frontend-packages environment: NODE_OPTIONS: --max_old_space_size=4096 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-storybook when: event: @@ -2223,7 +2278,7 @@ steps: from_secret: gcp_key PRERELEASE_BUCKET: from_secret: prerelease_bucket - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: store-npm-packages trigger: event: @@ -2272,13 +2327,13 @@ steps: - commands: - yarn install --immutable depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: yarn-install - commands: - yarn betterer ci depends_on: - yarn-install - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: betterer-frontend - commands: - yarn run ci:test-frontend @@ -2286,7 +2341,7 @@ steps: - yarn-install environment: TEST_MAX_WORKERS: 50% - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: test-frontend trigger: event: @@ -2334,7 +2389,7 @@ steps: in output.' - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -2342,25 +2397,25 @@ steps: in output.' - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-jsonnet - commands: - make gen-go depends_on: - verify-gen-cue - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: wire-install - commands: - go test -short -covermode=atomic -timeout=5m ./pkg/... depends_on: - wire-install - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: test-backend - commands: - go test -run Integration -covermode=atomic -timeout=5m ./pkg/... depends_on: - wire-install - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: test-backend-integration trigger: event: @@ -2427,7 +2482,7 @@ steps: in output.' - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -2435,13 +2490,13 @@ steps: in output.' - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-jsonnet - commands: - make gen-go depends_on: - verify-gen-cue - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: wire-install - commands: - apt-get update @@ -2457,7 +2512,7 @@ steps: GRAFANA_TEST_DB: postgres PGPASSWORD: grafanatest POSTGRES_HOST: postgres - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: postgres-integration-tests - commands: - apt-get update @@ -2473,7 +2528,7 @@ steps: environment: GRAFANA_TEST_DB: mysql MYSQL_HOST: mysql - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: mysql-integration-tests trigger: event: @@ -2590,7 +2645,7 @@ steps: environment: GITHUB_TOKEN: from_secret: github_token - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: clone-enterprise - commands: - mv bin/grabpl /tmp/ @@ -2606,7 +2661,7 @@ steps: environment: GITHUB_TOKEN: from_secret: github_token - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: init-enterprise - commands: - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd @@ -2620,13 +2675,13 @@ steps: - make gen-go depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: wire-install - commands: - yarn install --immutable depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: yarn-install - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -2636,7 +2691,7 @@ steps: - CODEGEN_VERIFY=1 make gen-cue depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -2645,14 +2700,14 @@ steps: - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-jsonnet - commands: - ./bin/build build-backend --jobs 8 --edition enterprise ${DRONE_TAG} depends_on: - wire-install - compile-build-cmd - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-backend - commands: - ./bin/build build-frontend --jobs 8 --edition enterprise ${DRONE_TAG} @@ -2661,7 +2716,7 @@ steps: - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-frontend - commands: - ./bin/build build-frontend-packages --jobs 8 --edition enterprise ${DRONE_TAG} @@ -2670,7 +2725,7 @@ steps: - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-frontend-packages - commands: - ./bin/build build-plugins --jobs 8 --edition enterprise @@ -2680,7 +2735,7 @@ steps: environment: GRAFANA_API_KEY: from_secret: grafana_api_key - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-plugins - commands: - ./bin/build package --jobs 8 --edition enterprise --sign ${DRONE_TAG} @@ -2698,14 +2753,14 @@ steps: from_secret: packages_gpg_public_key GRAFANA_API_KEY: from_secret: grafana_api_key - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: package - commands: - ls dist/*.tar.gz* - cp dist/*.tar.gz* packaging/docker/ depends_on: - package - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: copy-packages-for-docker - commands: - ./bin/build build-docker --edition enterprise --shouldSave @@ -2745,7 +2800,7 @@ steps: ARCH: linux-amd64 PORT: 3001 RUNDIR: scripts/grafana-server/tmp-grafana-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: grafana-server - commands: - apt-get install -y netcat @@ -2877,7 +2932,7 @@ steps: environment: GITHUB_TOKEN: from_secret: github_token - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: clone-enterprise - commands: - mv bin/grabpl /tmp/ @@ -2893,7 +2948,7 @@ steps: environment: GITHUB_TOKEN: from_secret: github_token - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: init-enterprise - commands: - echo $DRONE_RUNNER_NAME @@ -2909,14 +2964,14 @@ steps: - yarn install --immutable depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: yarn-install - commands: - yarn betterer ci depends_on: - init-enterprise - yarn-install - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: betterer-frontend - commands: - yarn run ci:test-frontend @@ -2925,7 +2980,7 @@ steps: - yarn-install environment: TEST_MAX_WORKERS: 50% - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: test-frontend trigger: event: @@ -2962,7 +3017,7 @@ steps: environment: GITHUB_TOKEN: from_secret: github_token - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: clone-enterprise - commands: - mkdir -p bin @@ -2984,7 +3039,7 @@ steps: environment: GITHUB_TOKEN: from_secret: github_token - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: init-enterprise - commands: - echo $DRONE_RUNNER_NAME @@ -3006,7 +3061,7 @@ steps: - CODEGEN_VERIFY=1 make gen-cue depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -3015,25 +3070,25 @@ steps: - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-jsonnet - commands: - make gen-go depends_on: - verify-gen-cue - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: wire-install - commands: - go test -short -covermode=atomic -timeout=5m ./pkg/... depends_on: - wire-install - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: test-backend - commands: - go test -run Integration -covermode=atomic -timeout=5m ./pkg/... depends_on: - wire-install - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: test-backend-integration trigger: event: @@ -3106,7 +3161,7 @@ steps: environment: GITHUB_TOKEN: from_secret: github_token - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: clone-enterprise - commands: - mv bin/grabpl /tmp/ @@ -3122,7 +3177,7 @@ steps: environment: GITHUB_TOKEN: from_secret: github_token - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: init-enterprise - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -3132,7 +3187,7 @@ steps: - CODEGEN_VERIFY=1 make gen-cue depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -3141,13 +3196,13 @@ steps: - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-jsonnet - commands: - make gen-go depends_on: - verify-gen-cue - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: wire-install - commands: - apt-get update @@ -3163,7 +3218,7 @@ steps: GRAFANA_TEST_DB: postgres PGPASSWORD: grafanatest POSTGRES_HOST: postgres - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: postgres-integration-tests - commands: - apt-get update @@ -3179,18 +3234,16 @@ steps: environment: GRAFANA_TEST_DB: mysql MYSQL_HOST: mysql - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: mysql-integration-tests - commands: - dockerize -wait tcp://redis:6379/0 -timeout 120s - - go clean -testcache - - go list './pkg/...' | xargs -I {} sh -c 'go test -run Integration -covermode=atomic - -timeout=5m {}' + - ./bin/grabpl integration-tests depends_on: - wire-install environment: REDIS_URL: redis://redis:6379/0 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: redis-integration-tests - commands: - dockerize -wait tcp://memcached:11211 -timeout 120s @@ -3201,7 +3254,7 @@ steps: - wire-install environment: MEMCACHED_HOSTS: memcached:11211 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: memcached-integration-tests trigger: event: @@ -3338,7 +3391,7 @@ steps: environment: GITHUB_TOKEN: from_secret: github_token - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: clone-enterprise - commands: - mv bin/grabpl /tmp/ @@ -3354,7 +3407,7 @@ steps: environment: GITHUB_TOKEN: from_secret: github_token - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: init-enterprise - commands: - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd @@ -3368,13 +3421,13 @@ steps: - make gen-go depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: wire-install - commands: - yarn install --immutable depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: yarn-install - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -3384,7 +3437,7 @@ steps: - CODEGEN_VERIFY=1 make gen-cue depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-cue - commands: - ./bin/build build-frontend --jobs 8 --edition enterprise ${DRONE_TAG} @@ -3393,7 +3446,7 @@ steps: - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-frontend - commands: - ./bin/build build-frontend-packages --jobs 8 --edition enterprise ${DRONE_TAG} @@ -3402,7 +3455,7 @@ steps: - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-frontend-packages - commands: - ./bin/build build-plugins --jobs 8 --edition enterprise @@ -3412,14 +3465,14 @@ steps: environment: GRAFANA_API_KEY: from_secret: grafana_api_key - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-plugins - commands: - ./bin/build build-backend --jobs 8 --edition enterprise2 ${DRONE_TAG} depends_on: - wire-install - compile-build-cmd - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-backend-enterprise2 - commands: - ./bin/build package --jobs 8 --edition enterprise2 --sign ${DRONE_TAG} @@ -3437,7 +3490,7 @@ steps: from_secret: packages_gpg_public_key GRAFANA_API_KEY: from_secret: grafana_api_key - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: package-enterprise2 - commands: - ./bin/build upload-cdn --edition enterprise2 @@ -3457,7 +3510,7 @@ steps: - cp dist/*.tar.gz* packaging/docker/ depends_on: - package-enterprise2 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: copy-packages-for-docker - commands: - ./bin/build build-docker --edition enterprise2 --shouldSave @@ -3588,7 +3641,7 @@ steps: environment: GITHUB_TOKEN: from_secret: github_token - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: clone-enterprise - commands: - mv bin/grabpl /tmp/ @@ -3604,7 +3657,7 @@ steps: environment: GITHUB_TOKEN: from_secret: github_token - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: init-enterprise - commands: - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd @@ -3618,13 +3671,13 @@ steps: - make gen-go depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: wire-install - commands: - yarn install --immutable depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: yarn-install - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -3634,7 +3687,7 @@ steps: - CODEGEN_VERIFY=1 make gen-cue depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-cue - commands: - ./bin/build build-frontend --jobs 8 --edition enterprise ${DRONE_TAG} @@ -3643,7 +3696,7 @@ steps: - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-frontend - commands: - ./bin/build build-frontend-packages --jobs 8 --edition enterprise ${DRONE_TAG} @@ -3652,7 +3705,7 @@ steps: - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-frontend-packages - commands: - ./bin/build build-plugins --jobs 8 --edition enterprise @@ -3662,14 +3715,14 @@ steps: environment: GRAFANA_API_KEY: from_secret: grafana_api_key - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-plugins - commands: - ./bin/build build-backend --jobs 8 --edition enterprise2 ${DRONE_TAG} depends_on: - wire-install - compile-build-cmd - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-backend-enterprise2 - commands: - ./bin/build package --jobs 8 --edition enterprise2 --sign ${DRONE_TAG} @@ -3687,7 +3740,7 @@ steps: from_secret: packages_gpg_public_key GRAFANA_API_KEY: from_secret: grafana_api_key - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: package-enterprise2 - commands: - ./bin/build upload-cdn --edition enterprise2 @@ -3707,7 +3760,7 @@ steps: - cp dist/*.tar.gz* packaging/docker/ depends_on: - package-enterprise2 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: copy-packages-for-docker - commands: - ./bin/build build-docker --edition enterprise2 --shouldSave @@ -4269,20 +4322,27 @@ platform: services: [] steps: - commands: - - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.20/grabpl - - chmod +x bin/grabpl - image: byrnedo/alpine-curl:0.1.8 - name: grabpl -- commands: - - ./bin/grabpl artifacts publish --security --tag $${DRONE_TAG} --src-bucket $${PRERELEASE_BUCKET} - depends_on: - - grabpl + - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd + depends_on: [] environment: + CGO_ENABLED: 0 + image: golang:1.19.4 + name: compile-build-cmd +- commands: + - ./bin/build artifacts publish --security --tag $${DRONE_TAG} --src-bucket $${PRERELEASE_BUCKET} + depends_on: + - compile-build-cmd + environment: + ENTERPRISE2_SECURITY_PREFIX: + from_secret: enterprise2_security_prefix GCP_KEY: from_secret: gcp_key PRERELEASE_BUCKET: from_secret: prerelease_bucket + SECURITY_DEST_BUCKET: + from_secret: security_dest_bucket + STATIC_ASSET_EDITIONS: + from_secret: static_asset_editions image: grafana/grafana-ci-deploy:1.3.3 name: publish-artifacts trigger: @@ -4313,20 +4373,27 @@ platform: services: [] steps: - commands: - - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.20/grabpl - - chmod +x bin/grabpl - image: byrnedo/alpine-curl:0.1.8 - name: grabpl -- commands: - - ./bin/grabpl artifacts publish --tag $${DRONE_TAG} --src-bucket $${PRERELEASE_BUCKET} - depends_on: - - grabpl + - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd + depends_on: [] environment: + CGO_ENABLED: 0 + image: golang:1.19.4 + name: compile-build-cmd +- commands: + - ./bin/build artifacts publish --tag $${DRONE_TAG} --src-bucket $${PRERELEASE_BUCKET} + depends_on: + - compile-build-cmd + environment: + ENTERPRISE2_SECURITY_PREFIX: + from_secret: enterprise2_security_prefix GCP_KEY: from_secret: gcp_key PRERELEASE_BUCKET: from_secret: prerelease_bucket + SECURITY_DEST_BUCKET: + from_secret: security_dest_bucket + STATIC_ASSET_EDITIONS: + from_secret: static_asset_editions image: grafana/grafana-ci-deploy:1.3.3 name: publish-artifacts trigger: @@ -4366,7 +4433,7 @@ steps: - commands: - yarn install --immutable depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: yarn-install - commands: - ./bin/build artifacts npm retrieve --tag ${DRONE_TAG} @@ -4390,7 +4457,7 @@ steps: NPM_TOKEN: from_secret: npm_token failure: ignore - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: release-npm-packages trigger: event: @@ -4626,7 +4693,7 @@ steps: environment: GCP_KEY: from_secret: gcp_key - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: artifacts-page trigger: event: @@ -4671,18 +4738,18 @@ steps: in output.' - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-cue - commands: - make gen-go depends_on: - verify-gen-cue - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: wire-install - commands: - yarn install --immutable depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: yarn-install - commands: - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd @@ -4696,7 +4763,7 @@ steps: depends_on: - wire-install - compile-build-cmd - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-backend - commands: - ./bin/build build-frontend --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER} @@ -4705,7 +4772,7 @@ steps: - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-frontend - commands: - ./bin/build build-frontend-packages --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER} @@ -4714,7 +4781,7 @@ steps: - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-frontend-packages - commands: - ./bin/build build-plugins --jobs 8 --edition oss @@ -4724,7 +4791,7 @@ steps: environment: GRAFANA_API_KEY: from_secret: grafana_api_key - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-plugins - commands: - ./bin/build package --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER} --sign @@ -4742,14 +4809,14 @@ steps: from_secret: packages_gpg_public_key GRAFANA_API_KEY: from_secret: grafana_api_key - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: package - commands: - ls dist/*.tar.gz* - cp dist/*.tar.gz* packaging/docker/ depends_on: - package - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: copy-packages-for-docker - commands: - ./bin/build build-docker --edition oss --shouldSave @@ -4788,7 +4855,7 @@ steps: environment: ARCH: linux-amd64 PORT: 3001 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: grafana-server - commands: - apt-get install -y netcat @@ -4865,7 +4932,7 @@ steps: - build-frontend-packages environment: NODE_OPTIONS: --max_old_space_size=4096 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-storybook when: paths: @@ -4946,13 +5013,13 @@ steps: - commands: - yarn install --immutable depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: yarn-install - commands: - yarn betterer ci depends_on: - yarn-install - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: betterer-frontend - commands: - yarn run ci:test-frontend @@ -4960,7 +5027,7 @@ steps: - yarn-install environment: TEST_MAX_WORKERS: 50% - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: test-frontend trigger: ref: @@ -5005,7 +5072,7 @@ steps: in output.' - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -5013,25 +5080,25 @@ steps: in output.' - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-jsonnet - commands: - make gen-go depends_on: - verify-gen-cue - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: wire-install - commands: - go test -short -covermode=atomic -timeout=5m ./pkg/... depends_on: - wire-install - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: test-backend - commands: - go test -run Integration -covermode=atomic -timeout=5m ./pkg/... depends_on: - wire-install - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: test-backend-integration trigger: ref: @@ -5095,7 +5162,7 @@ steps: in output.' - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -5103,13 +5170,13 @@ steps: in output.' - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-jsonnet - commands: - make gen-go depends_on: - verify-gen-cue - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: wire-install - commands: - apt-get update @@ -5125,7 +5192,7 @@ steps: GRAFANA_TEST_DB: postgres PGPASSWORD: grafanatest POSTGRES_HOST: postgres - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: postgres-integration-tests - commands: - apt-get update @@ -5141,7 +5208,7 @@ steps: environment: GRAFANA_TEST_DB: mysql MYSQL_HOST: mysql - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: mysql-integration-tests trigger: ref: @@ -5248,7 +5315,7 @@ steps: environment: GITHUB_TOKEN: from_secret: github_token - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: clone-enterprise - commands: - mv bin/grabpl /tmp/ @@ -5263,7 +5330,7 @@ steps: environment: GITHUB_TOKEN: from_secret: github_token - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: init-enterprise - commands: - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd @@ -5277,13 +5344,13 @@ steps: - make gen-go depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: wire-install - commands: - yarn install --immutable depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: yarn-install - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -5293,7 +5360,7 @@ steps: - CODEGEN_VERIFY=1 make gen-cue depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -5302,14 +5369,14 @@ steps: - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-jsonnet - commands: - ./bin/build build-backend --jobs 8 --edition enterprise --build-id ${DRONE_BUILD_NUMBER} depends_on: - wire-install - compile-build-cmd - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-backend - commands: - ./bin/build build-frontend --jobs 8 --edition enterprise --build-id ${DRONE_BUILD_NUMBER} @@ -5318,7 +5385,7 @@ steps: - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-frontend - commands: - ./bin/build build-frontend-packages --jobs 8 --edition enterprise --build-id ${DRONE_BUILD_NUMBER} @@ -5327,7 +5394,7 @@ steps: - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-frontend-packages - commands: - ./bin/build build-plugins --jobs 8 --edition enterprise @@ -5337,7 +5404,7 @@ steps: environment: GRAFANA_API_KEY: from_secret: grafana_api_key - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-plugins - commands: - ./bin/build package --jobs 8 --edition enterprise --build-id ${DRONE_BUILD_NUMBER} @@ -5356,14 +5423,14 @@ steps: from_secret: packages_gpg_public_key GRAFANA_API_KEY: from_secret: grafana_api_key - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: package - commands: - ls dist/*.tar.gz* - cp dist/*.tar.gz* packaging/docker/ depends_on: - package - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: copy-packages-for-docker - commands: - ./bin/build build-docker --edition enterprise --shouldSave @@ -5403,7 +5470,7 @@ steps: ARCH: linux-amd64 PORT: 3001 RUNDIR: scripts/grafana-server/tmp-grafana-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: grafana-server - commands: - apt-get install -y netcat @@ -5538,7 +5605,7 @@ steps: environment: GITHUB_TOKEN: from_secret: github_token - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: clone-enterprise - commands: - mv bin/grabpl /tmp/ @@ -5553,7 +5620,7 @@ steps: environment: GITHUB_TOKEN: from_secret: github_token - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: init-enterprise - commands: - echo $DRONE_RUNNER_NAME @@ -5569,14 +5636,14 @@ steps: - yarn install --immutable depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: yarn-install - commands: - yarn betterer ci depends_on: - init-enterprise - yarn-install - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: betterer-frontend - commands: - yarn run ci:test-frontend @@ -5585,7 +5652,7 @@ steps: - yarn-install environment: TEST_MAX_WORKERS: 50% - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: test-frontend trigger: ref: @@ -5619,7 +5686,7 @@ steps: environment: GITHUB_TOKEN: from_secret: github_token - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: clone-enterprise - commands: - mkdir -p bin @@ -5640,7 +5707,7 @@ steps: environment: GITHUB_TOKEN: from_secret: github_token - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: init-enterprise - commands: - echo $DRONE_RUNNER_NAME @@ -5662,7 +5729,7 @@ steps: - CODEGEN_VERIFY=1 make gen-cue depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -5671,25 +5738,25 @@ steps: - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-jsonnet - commands: - make gen-go depends_on: - verify-gen-cue - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: wire-install - commands: - go test -short -covermode=atomic -timeout=5m ./pkg/... depends_on: - wire-install - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: test-backend - commands: - go test -run Integration -covermode=atomic -timeout=5m ./pkg/... depends_on: - wire-install - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: test-backend-integration trigger: ref: @@ -5759,7 +5826,7 @@ steps: environment: GITHUB_TOKEN: from_secret: github_token - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: clone-enterprise - commands: - mv bin/grabpl /tmp/ @@ -5774,7 +5841,7 @@ steps: environment: GITHUB_TOKEN: from_secret: github_token - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: init-enterprise - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -5784,7 +5851,7 @@ steps: - CODEGEN_VERIFY=1 make gen-cue depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -5793,13 +5860,13 @@ steps: - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-jsonnet - commands: - make gen-go depends_on: - verify-gen-cue - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: wire-install - commands: - apt-get update @@ -5815,7 +5882,7 @@ steps: GRAFANA_TEST_DB: postgres PGPASSWORD: grafanatest POSTGRES_HOST: postgres - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: postgres-integration-tests - commands: - apt-get update @@ -5831,18 +5898,16 @@ steps: environment: GRAFANA_TEST_DB: mysql MYSQL_HOST: mysql - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: mysql-integration-tests - commands: - dockerize -wait tcp://redis:6379/0 -timeout 120s - - go clean -testcache - - go list './pkg/...' | xargs -I {} sh -c 'go test -run Integration -covermode=atomic - -timeout=5m {}' + - ./bin/grabpl integration-tests depends_on: - wire-install environment: REDIS_URL: redis://redis:6379/0 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: redis-integration-tests - commands: - dockerize -wait tcp://memcached:11211 -timeout 120s @@ -5853,7 +5918,7 @@ steps: - wire-install environment: MEMCACHED_HOSTS: memcached:11211 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: memcached-integration-tests trigger: ref: @@ -5980,7 +6045,7 @@ steps: environment: GITHUB_TOKEN: from_secret: github_token - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: clone-enterprise - commands: - mv bin/grabpl /tmp/ @@ -5995,7 +6060,7 @@ steps: environment: GITHUB_TOKEN: from_secret: github_token - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: init-enterprise - commands: - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd @@ -6009,13 +6074,13 @@ steps: - make gen-go depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: wire-install - commands: - yarn install --immutable depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: yarn-install - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -6025,7 +6090,7 @@ steps: - CODEGEN_VERIFY=1 make gen-cue depends_on: - init-enterprise - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: verify-gen-cue - commands: - ./bin/build build-frontend --jobs 8 --edition enterprise --build-id ${DRONE_BUILD_NUMBER} @@ -6034,7 +6099,7 @@ steps: - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-frontend - commands: - ./bin/build build-frontend-packages --jobs 8 --edition enterprise --build-id ${DRONE_BUILD_NUMBER} @@ -6043,7 +6108,7 @@ steps: - yarn-install environment: NODE_OPTIONS: --max_old_space_size=8192 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-frontend-packages - commands: - ./bin/build build-plugins --jobs 8 --edition enterprise @@ -6053,7 +6118,7 @@ steps: environment: GRAFANA_API_KEY: from_secret: grafana_api_key - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-plugins - commands: - ./bin/build build-backend --jobs 8 --edition enterprise2 --build-id ${DRONE_BUILD_NUMBER} @@ -6061,7 +6126,7 @@ steps: depends_on: - wire-install - compile-build-cmd - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: build-backend-enterprise2 - commands: - ./bin/build package --jobs 8 --edition enterprise2 --build-id ${DRONE_BUILD_NUMBER} @@ -6080,7 +6145,7 @@ steps: from_secret: packages_gpg_public_key GRAFANA_API_KEY: from_secret: grafana_api_key - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: package-enterprise2 - commands: - ./bin/build upload-cdn --edition enterprise2 @@ -6100,7 +6165,7 @@ steps: - cp dist/*.tar.gz* packaging/docker/ depends_on: - package-enterprise2 - image: grafana/build-container:1.6.7 + image: grafana/build-container:v1.7.1 name: copy-packages-for-docker - commands: - ./bin/build build-docker --edition enterprise2 --shouldSave @@ -6451,6 +6516,6 @@ kind: secret name: aws_secret_access_key --- kind: signature -hmac: 86222a86386ae1d2afce23b4a15f09e97aaeb873275ed34ac73d733335d63e4b +hmac: eba6c445aae6d75df0a2963d5e1e90c44474587a9e1d11e21bf2ba4d99f14da8 ... diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 97f26eee916..84dec49745d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -12,26 +12,26 @@ # This should make it easy to add new rules without breaking existing ones. # Documentation -/docs/ @grafana/docs-squad -/contribute/ @grafana/docs-squad -/docs/sources/developers/plugins/ @grafana/docs-squad @grafana/plugins-platform-frontend @grafana/plugins-platform-backend -/docs/sources/developers/plugins/backend/ @grafana/docs-squad @grafana/plugins-platform-backend -/.changelog-archive @grafana/docs-squad -CHANGELOG.md @grafana/docs-squad -CODE_OF_CONDUCT.md @grafana/docs-squad -CONTRIBUTING.md @grafana/docs-squad +/docs/ @grafana/docs-grafana +/contribute/ @grafana/docs-grafana +/docs/sources/developers/plugins/ @grafana/docs-grafana @grafana/plugins-platform-frontend @grafana/plugins-platform-backend +/docs/sources/developers/plugins/backend/ @grafana/docs-grafana @grafana/plugins-platform-backend +/.changelog-archive @grafana/docs-grafana +CHANGELOG.md @grafana/docs-grafana +CODE_OF_CONDUCT.md @grafana/docs-grafana +CONTRIBUTING.md @grafana/docs-grafana GOVERNANCE.md @RichiH -HALL_OF_FAME.md @grafana/docs-squad +HALL_OF_FAME.md @grafana/docs-grafana ISSUE_TRIAGE.md @grafana/grafana-community-support LICENSE @torkelo LICENSING.md @torkelo MAINTAINERS.md @RichiH NOTICE.md @torkelo -README.md @grafana/docs-squad +README.md @grafana/docs-grafana ROADMAP.md @torkelo SECURITY.md @grafana/security-team SUPPORT.md @torkelo -UPGRADING_DEPENDENCIES.md @grafana/docs-squad +UPGRADING_DEPENDENCIES.md @grafana/docs-grafana WORKFLOW.md @torkelo @@ -112,7 +112,7 @@ WORKFLOW.md @torkelo /pkg/services/pluginsettings/ @grafana/backend-platform /pkg/services/preference/ @grafana/backend-platform /pkg/services/provisioning/ @grafana/backend-platform -/pkg/services/publicdashboards/ @grafana/backend-platform +/pkg/services/publicdashboards/ @grafana/dashboards-squad /pkg/services/query/ @grafana/backend-platform /pkg/services/queryhistory/ @grafana/backend-platform /pkg/services/quota/ @grafana/backend-platform @@ -306,7 +306,6 @@ WORKFLOW.md @torkelo /packages/grafana-ui/src/components/TimeSeries/ @grafana/grafana-bi-squad /packages/grafana-ui/src/components/uPlot/ @grafana/grafana-bi-squad /packages/grafana-ui/src/utils/storybook/ @grafana/plugins-platform-frontend -/packages/jaeger-ui-components// @grafana/observability-traces-and-profiling /packages/grafana-data/src/**/*logs* @grafana/observability-logs /plugins-bundled/ @grafana/plugins-platform-frontend @@ -436,6 +435,8 @@ lerna.json @grafana/frontend-ops /public/app/features/explore/Logs.tsx @grafana/observability-logs /public/app/features/explore/LogsContainer.tsx @grafana/observability-logs +/public/app/features/explore/TraceView/ @grafana/observability-traces-and-profiling + /public/api-merged.json @grafana/backend-platform /public/api-spec.json @grafana/backend-platform /public/openapi3.json @grafana/backend-platform @@ -570,12 +571,12 @@ embed.go @grafana/grafana-as-code /.github/workflows/backport.yml @grafana/grafana-release-eng /.github/workflows/bump-version.yml @grafana/grafana-release-eng /.github/workflows/close-milestone.yml @grafana/grafana-release-eng -/.github/workflows/cloud-data-sources-code-coverage.yml @grafana/partner-plugins +/.github/workflows/cloud-data-sources-code-coverage.yml @grafana/partner-plugins @grafana/aws-plugins /.github/workflows/codeowners-validator.yml @tolzhabayev /.github/workflows/codeql-analysis.yml @DanCech /.github/workflows/commands.yml @torkelo /.github/workflows/detect-breaking-changes-* @grafana/plugins-platform-frontend -/.github/workflows/doc-validator.yml @grafana/docs-squad +/.github/workflows/doc-validator.yml @grafana/docs-grafana /.github/workflows/enterprise-pr-check.yml @grafana/grafana-release-eng /.github/workflows/epic-add-to-platform-ux-parent-project.yml @meanmina /.github/workflows/github-release.yml @torkelo @@ -589,8 +590,8 @@ embed.go @grafana/grafana-as-code /.github/workflows/pr-codeql-analysis-python.yml @DanCech /.github/workflows/pr-commands-closed.yml @tolzhabayev /.github/workflows/pr-commands.yml @marefr -/.github/workflows/publish-technical-documentation-next.yml @grafana/docs-squad -/.github/workflows/publish-technical-documentation-release.yml @grafana/docs-squad +/.github/workflows/publish-technical-documentation-next.yml @grafana/docs-grafana +/.github/workflows/publish-technical-documentation-release.yml @grafana/docs-grafana /.github/workflows/remove-milestone.yml @grafana/user-essentials /.github/workflows/sbom-report.yml @grafana/security-team /.github/workflows/scripts/json-file-to-job-output.js @grafana/plugins-platform-frontend @@ -610,6 +611,3 @@ embed.go @grafana/grafana-as-code /conf/provisioning/datasources/ @grafana/plugins-platform-backend /conf/provisioning/notifiers/ @bergquist /conf/provisioning/plugins/ @grafana/plugins-platform-backend - - - diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 1ed4455944f..c9831f8b844 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -5,19 +5,15 @@ "enabledManagers": ["npm"], "ignoreDeps": [ "@types/systemjs", - "@types/react-icons", // jaeger-ui-components is being refactored to use @grafana/ui icons instead "commander", // we are planning to remove this, so no need to update it "execa", // we should bump this once we move to esm modules "history", // we should bump this together with react-router-dom "@mdx-js/react", // storybook peer-depends on its 1.x version, we should upgrade this when we upgrade storybook "monaco-editor", // due to us exposing this via @grafana/ui/CodeEditor's props bumping can break plugins - "node-fetch", // we should bump this once we move to esm modules "react-hook-form", // due to us exposing these hooks via @grafana/ui form components bumping can break plugins - "react-icons", // jaeger-ui-components is being refactored to use @grafana/ui icons instead "react-redux", // react-beautiful-dnd depends on react-redux 7.x, we need to update that one first "react-router-dom", // we should bump this together with history "systemjs", - "copy-webpack-plugin", // try to upgrade with newer yarn release. Not working with 3.1.1 "ts-loader", // we should remove ts-loader and use babel-loader instead "ora", // we should bump this once we move to esm modules diff --git a/.github/workflows/cloud-data-sources-code-coverage.yml b/.github/workflows/cloud-data-sources-code-coverage.yml index a69cb01601a..bb27cb086de 100644 --- a/.github/workflows/cloud-data-sources-code-coverage.yml +++ b/.github/workflows/cloud-data-sources-code-coverage.yml @@ -14,7 +14,7 @@ on: jobs: workflow-call: - uses: grafana/code-coverage/.github/workflows/code-coverage.yml@v0.1.15 + uses: grafana/code-coverage/.github/workflows/code-coverage.yml@v0.1.17 with: frontend-path-regexp: public\/app\/plugins\/datasource\/(grafana-azure-monitor-datasource|cloud-monitoring|cloudwatch) backend-path-regexp: pkg\/tsdb\/(azuremonitor|cloudmonitoring|cloudwatch) diff --git a/.github/workflows/ox-code-coverage.yml b/.github/workflows/ox-code-coverage.yml index e852c06570b..7413350f646 100644 --- a/.github/workflows/ox-code-coverage.yml +++ b/.github/workflows/ox-code-coverage.yml @@ -15,7 +15,7 @@ on: jobs: workflow-call: - uses: grafana/code-coverage/.github/workflows/code-coverage.yml@v0.1.15 + uses: grafana/code-coverage/.github/workflows/code-coverage.yml@v0.1.17 with: frontend-path-regexp: public\/app\/features\/(explore|correlations)|public\/app\/plugins\/datasource\/(loki|elasticsearch) backend-path-regexp: pkg\/services\/(queryhistory)|pkg\/tsdb\/(loki|elasticsearch) diff --git a/.gitignore b/.gitignore index a4c02bfa4ca..7c44ef5bdd0 100644 --- a/.gitignore +++ b/.gitignore @@ -157,6 +157,8 @@ compilation-stats.json /e2e/benchmarks/**/results/* /e2e/benchmarks/**/results /e2e/build_results.zip +/e2e/extensions +/e2e/extensions-suite # grafana server /scripts/grafana-server/server.log diff --git a/LICENSING.md b/LICENSING.md index 7b64973e012..696cf188f9b 100644 --- a/LICENSING.md +++ b/LICENSING.md @@ -15,7 +15,6 @@ packages/grafana-e2e-selectors/ packages/grafana-runtime/ packages/grafana-toolkit/ packages/grafana-ui/ -packages/jaeger-ui-components/ packaging/ kinds/ pkg/kinds/ @@ -23,6 +22,7 @@ pkg/kindsys/ pkg/registry/corekind/ grafana-mixin/ public/app/plugins/datasource/tempo +public/app/features/explore/TraceView/components public/img/icons/solid/ public/img/icons/unicons/ ``` diff --git a/Makefile b/Makefile index 78cb6f15963..49acbcf06fa 100644 --- a/Makefile +++ b/Makefile @@ -239,8 +239,12 @@ drone: $(DRONE) $(DRONE) lint .drone.yml --trusted $(DRONE) --server https://drone.grafana.net sign --save grafana/grafana +# Generate an Emacs tags table (https://www.gnu.org/software/emacs/manual/html_node/emacs/Tags-Tables.html) for Starlark files. +scripts/drone/TAGS: $(shell find scripts/drone -name '*.star') + etags --lang none --regex="/def \(\w+\)[^:]+:/\1/" --regex="/\s*\(\w+\) =/\1/" $^ -o $@ + format-drone: - black --include '\.star$$' -S scripts/drone/ .drone.star + buildifier -r scripts/drone help: ## Display this help. @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) diff --git a/conf/defaults.ini b/conf/defaults.ini index d67385d1db5..539e1bee548 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -423,7 +423,7 @@ verify_email_enabled = false login_hint = email or username password_hint = password -# Default UI theme ("dark" or "light") +# Default UI theme ("dark" or "light" or "system") default_theme = dark # Default UI language (supported IETF language tag, such as en-US) @@ -621,6 +621,7 @@ allowed_groups = role_attribute_path = role_attribute_strict = false allow_assign_grafana_admin = false +skip_org_role_sync = false #################################### Generic OAuth ####################### [auth.generic_oauth] diff --git a/conf/provisioning/alerting/sample.yaml b/conf/provisioning/alerting/sample.yaml index 2a86bf5ea71..bce60aacd7f 100644 --- a/conf/provisioning/alerting/sample.yaml +++ b/conf/provisioning/alerting/sample.yaml @@ -11,7 +11,7 @@ apiVersion: 1 # folder: my_first_folder # # interval of the rule group evaluation # interval: 60s -# # list of rules that are part of the rule group +# # list of rules that are part of the rule group # rules: # # unique identifier for the rule # - uid: my_id_1 @@ -53,7 +53,7 @@ apiVersion: 1 # # state of the alert rule when no data is returned # # possible values: "NoData", "Alerting", "OK", default = NoData # noDataState: Alerting -# # state of the alert rule when the query execution +# # state of the alert rule when the query execution # # fails - possible values: "Error", "Alerting", "OK" # # default = Alerting # executionErrorState: Alerting @@ -62,10 +62,11 @@ apiVersion: 1 # # > map of strings to attach arbitrary custom data # annotations: # some_key: some_value -# # map of strings to filter and +# # map of strings to filter and # # route alerts # labels: # team: sre_team_1 +# isPaused: false # # List of alert rule UIDs that should be deleted # deleteRules: @@ -103,7 +104,7 @@ apiVersion: 1 # # > The labels by which incoming alerts are grouped together. For example, # # multiple alerts coming in for cluster=A and alertname=LatencyHigh would # # be batched into a single group. -# # +# # # # To aggregate by all possible labels, use the special value '...' as # # the sole label name, for example: # # group_by: ['...'] @@ -127,7 +128,7 @@ apiVersion: 1 # mute_time_intervals: # - abc # # How long to initially wait to send a notification for a group -# # of alerts. Allows to collect more initial alerts for the same group. +# # of alerts. Allows to collect more initial alerts for the same group. # # (Usually ~0s to few minutes), default = 30s # group_wait: 30s # # How long to wait before sending a notification about new alerts that @@ -138,7 +139,7 @@ apiVersion: 1 # # been sent successfully for an alert. (Usually ~3h or more), default = 4h # repeat_interval: 4h # # Zero or more child routes -# routes: +# routes: # ... # # List of orgIds that should be reset to the default policy diff --git a/conf/sample.ini b/conf/sample.ini index 546f828ac45..250205753bf 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -616,6 +616,7 @@ ;role_attribute_path = ;role_attribute_strict = false ;allow_assign_grafana_admin = false +;skip_org_role_sync = false #################################### Generic OAuth ########################## [auth.generic_oauth] diff --git a/devenv/dev-dashboards/panel-timeline/timeline-thresholds-mappings.json b/devenv/dev-dashboards/panel-timeline/timeline-thresholds-mappings.json new file mode 100644 index 00000000000..be7a3567bfa --- /dev/null +++ b/devenv/dev-dashboards/panel-timeline/timeline-thresholds-mappings.json @@ -0,0 +1,761 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 1263, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "fillOpacity": 70, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 6, + "x": 0, + "y": 0 + }, + "id": 11, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0" + } + ], + "title": "default", + "type": "state-timeline" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "fillOpacity": 70, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 10 + }, + { + "color": "#EAB839", + "value": 20 + }, + { + "color": "#6ED0E0", + "value": 30 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 6, + "x": 6, + "y": 0 + }, + "id": 2, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "5,10,20,30,40" + } + ], + "title": "default absolute thresholds", + "type": "state-timeline" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "fillOpacity": 70, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [], + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 10 + }, + { + "color": "#EAB839", + "value": 20 + }, + { + "color": "#6ED0E0", + "value": 30 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 6, + "x": 12, + "y": 0 + }, + "id": 8, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "5,10,20,30,40" + } + ], + "title": "default percentage thresholds", + "type": "state-timeline" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "fillOpacity": 70, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "A-series" + }, + "properties": [ + { + "id": "thresholds", + "value": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 10 + }, + { + "color": "#EAB839", + "value": 20 + }, + { + "color": "#6ED0E0", + "value": 30 + } + ] + } + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 6, + "x": 18, + "y": 0 + }, + "id": 3, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "5,10,20,30,40" + } + ], + "title": "override thresholds", + "type": "state-timeline" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "fillOpacity": 70, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [ + { + "options": { + "from": 0, + "result": { + "color": "green", + "index": 0 + }, + "to": 9.9999 + }, + "type": "range" + }, + { + "options": { + "from": 10, + "result": { + "color": "yellow", + "index": 1 + }, + "to": 14.9999 + }, + "type": "range" + }, + { + "options": { + "from": 15, + "result": { + "color": "red", + "index": 2 + }, + "to": 24.9999 + }, + "type": "range" + }, + { + "options": { + "from": 25, + "result": { + "color": "blue", + "index": 3 + }, + "to": 100000 + }, + "type": "range" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 6, + "x": 0, + "y": 9 + }, + "id": 6, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "5,10,20,30,40" + } + ], + "title": "default value mappings", + "type": "state-timeline" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "fillOpacity": 70, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "A-series" + }, + "properties": [ + { + "id": "mappings", + "value": [ + { + "options": { + "from": 0, + "result": { + "color": "green", + "index": 0 + }, + "to": 9.9999 + }, + "type": "range" + }, + { + "options": { + "from": 10, + "result": { + "color": "yellow", + "index": 1 + }, + "to": 14.9999 + }, + "type": "range" + }, + { + "options": { + "from": 15, + "result": { + "color": "red", + "index": 2 + }, + "to": 24.9999 + }, + "type": "range" + }, + { + "options": { + "from": 25, + "result": { + "color": "blue", + "index": 3 + }, + "to": 100000 + }, + "type": "range" + } + ] + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 6, + "x": 6, + "y": 9 + }, + "id": 7, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "5,10,20,30,40" + } + ], + "title": "override value mappings", + "type": "state-timeline" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "fillOpacity": 70, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 6, + "x": 12, + "y": 9 + }, + "id": 5, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "rawFrameContent": "[\n {\n \"schema\": {\n \"refId\": \"A\",\n \"fields\": [\n {\n \"name\": \"time\",\n \"type\": \"time\",\n \"typeInfo\": {\n \"frame\": \"time\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"value\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"int64\",\n \"nullable\": true\n },\n \"config\": {\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"#EAB839\",\n \"value\": 10\n },\n {\n \"color\": \"red\",\n \"value\": 15\n },\n {\n \"color\": \"#6ED0E0\",\n \"value\": 25\n }\n ]\n }\n }\n }\n ]\n },\n \"data\": {\n \"values\": [\n [\n 1674732835000,\n 1674736435000,\n 1674740035000,\n 1674743635000\n ],\n [\n 5,\n 10,\n 20,\n 30\n ]\n ]\n }\n }\n]", + "refId": "A", + "scenarioId": "raw_frame" + } + ], + "title": "field thresholds from data", + "type": "state-timeline" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "fillOpacity": 70, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 6, + "x": 18, + "y": 9 + }, + "id": 9, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "5,10,20,30,40" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "hide": false, + "max": 30, + "min": 0.01, + "noise": 30, + "refId": "B", + "scenarioId": "random_walk", + "startValue": 1 + } + ], + "title": "threshold from random walk", + "transformations": [ + { + "id": "configFromData", + "options": { + "configRefId": "B", + "mappings": [ + { + "fieldName": "B-series", + "handlerKey": "threshold1" + } + ] + } + } + ], + "type": "state-timeline" + } + ], + "refresh": false, + "revision": 1, + "schemaVersion": 38, + "style": "dark", + "tags": [ + "gdev", + "panel-tests", + "state-timeline", + "graph-ng" + ], + "templating": { + "list": [] + }, + "time": { + "from": "2023-01-26T11:33:55.000Z", + "to": "2023-01-26T14:33:55.000Z" + }, + "timepicker": {}, + "timezone": "", + "title": "StateTimeline - Thresholds & Mappings", + "uid": "Kce7z9TVz", + "version": 14, + "weekStart": "" +} diff --git a/devenv/dev-dashboards/transforms/join-by-field.json b/devenv/dev-dashboards/transforms/join-by-field.json index 907cee7e5d1..a86b3b89a89 100644 --- a/devenv/dev-dashboards/transforms/join-by-field.json +++ b/devenv/dev-dashboards/transforms/join-by-field.json @@ -24,6 +24,7 @@ "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 0, + "id": 1351, "links": [], "liveNow": false, "panels": [ @@ -37,7 +38,7 @@ }, "id": 9, "panels": [], - "title": "Input", + "title": "Join by time", "type": "row" }, { @@ -48,225 +49,37 @@ "fieldConfig": { "defaults": { "color": { - "mode": "thresholds" + "mode": "palette-classic" }, "custom": { - "align": "auto", - "cellOptions": { - "type": "auto" + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false }, - "inspect": false - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 8, - "x": 0, - "y": 1 - }, - "id": 11, - "options": { - "footer": { - "countRows": false, - "fields": "", - "reducer": [ - "sum" - ], - "show": false - }, - "showHeader": true - }, - "pluginVersion": "9.4.0-pre", - "targets": [ - { - "datasource": { - "type": "testdata", - "uid": "PD8C576611E62080A" - }, - "rawFrameContent": "[{\r\n \"name\": \"tags\",\r\n \"fields\": [\r\n { \"name\": \"tags__time\", \"values\": [100, 101, 200] },\r\n { \"name\": \"tags__name\", \"values\": [\"v1.2\", \"v1.2b\", \"v1.3\"] }\r\n ]\r\n}]", - "refId": "tags", - "scenarioId": "raw_frame" - } - ], - "title": "tags", - "type": "table" - }, - { - "datasource": { - "type": "testdata", - "uid": "PD8C576611E62080A" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "custom": { - "align": "auto", - "cellOptions": { - "type": "auto" + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" }, - "inspect": false - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 8, - "x": 8, - "y": 1 - }, - "id": 13, - "options": { - "footer": { - "countRows": false, - "fields": "", - "reducer": [ - "sum" - ], - "show": false - }, - "showHeader": true - }, - "pluginVersion": "9.4.0-pre", - "targets": [ - { - "datasource": { - "type": "testdata", - "uid": "PD8C576611E62080A" - }, - "rawFrameContent": "[{\r\n \"name\": \"releases\",\r\n\"fields\": [\r\n { \"name\": \"releases__time\", \"values\": [150, 250] },\r\n { \"name\": \"releases__tag\", \"values\": [\"v1.2\", \"v1.3\"] }\r\n]}]", - "refId": "releases", - "scenarioId": "raw_frame" - } - ], - "title": "releases", - "type": "table" - }, - { - "datasource": { - "type": "testdata", - "uid": "PD8C576611E62080A" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "custom": { - "align": "auto", - "cellOptions": { - "type": "auto" + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" }, - "inspect": false - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 8, - "x": 16, - "y": 1 - }, - "id": 19, - "options": { - "footer": { - "countRows": false, - "fields": "", - "reducer": [ - "sum" - ], - "show": false - }, - "showHeader": true - }, - "pluginVersion": "9.4.0-pre", - "targets": [ - { - "datasource": { - "type": "testdata", - "uid": "PD8C576611E62080A" - }, - "rawFrameContent": "[{\r\n \"name\": \"features\",\r\n\"fields\": [\r\n { \"name\": \"features__name\", \"values\": [\"A\", \"B\", \"C\", \"D\", \"E\"] },\r\n { \"name\": \"features__tag\", \"values\": [\"v1.2\", \"v1.3\", \"v1.2b\", \"v1.3\", \"v1.2\"] }\r\n]}]", - "refId": "features", - "scenarioId": "raw_frame" - } - ], - "title": "features", - "type": "table" - }, - { - "collapsed": false, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 9 - }, - "id": 21, - "panels": [], - "title": "Output", - "type": "row" - }, - { - "datasource": { - "type": "testdata", - "uid": "PD8C576611E62080A" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "custom": { - "align": "auto", - "cellOptions": { - "type": "auto" - }, - "inspect": false + "thresholdsStyle": { + "mode": "off" + } }, "mappings": [], "thresholds": { @@ -288,71 +101,39 @@ "h": 8, "w": 12, "x": 0, - "y": 10 + "y": 1 }, - "id": 23, + "id": 11, "options": { - "footer": { - "countRows": false, - "fields": "", - "reducer": [ - "sum" - ], - "show": false + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true }, - "showHeader": true + "tooltip": { + "mode": "single", + "sort": "none" + } }, - "pluginVersion": "9.4.0-pre", "targets": [ { "datasource": { "type": "testdata", "uid": "PD8C576611E62080A" }, - "rawFrameContent": "[{\r\n \"name\": \"tags\",\r\n \"fields\": [\r\n { \"name\": \"tags__time\", \"values\": [100, 101, 200] },\r\n { \"name\": \"tags__name\", \"values\": [\"v1.2\", \"v1.2b\", \"v1.3\"] }\r\n ]\r\n}]", - "refId": "tags", - "scenarioId": "raw_frame" - }, - { - "datasource": { - "type": "testdata", - "uid": "PD8C576611E62080A" - }, - "rawFrameContent": "[{\r\n \"name\": \"releases\",\r\n \"fields\": [\r\n { \"name\": \"releases__time\", \"values\": [150, 250] },\r\n { \"name\": \"releases__tag\", \"values\": [\"v1.2\", \"v1.3\"] }\r\n]}]", - "refId": "releases", - "scenarioId": "raw_frame" - }, - { - "datasource": { - "type": "testdata", - "uid": "PD8C576611E62080A" - }, - "rawFrameContent": "[{\r\n \"name\": \"features\",\r\n \"fields\": [\r\n { \"name\": \"features__name\", \"values\": [\"A\", \"B\", \"C\", \"D\", \"E\"] },\r\n { \"name\": \"features__tag\", \"values\": [\"v1.2\", \"v1.3\", \"v1.2b\", \"v1.3\", \"v1.2\"] }\r\n]}]", - "refId": "features", - "scenarioId": "raw_frame" + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 4 } ], - "title": "OUTER JOIN", - "transformations": [ - { - "id": "joinByField", - "options": { - "fields": { - "A": "features__name", - "features": "features__tag", - "releases": "releases__tag", - "tags": "tags__name" - }, - "mode": "outer" - } - } - ], - "type": "table" + "title": "Timeseries data", + "type": "timeseries" }, { "datasource": { - "type": "testdata", - "uid": "PD8C576611E62080A" + "type": "datasource", + "uid": "-- Dashboard --" }, "fieldConfig": { "defaults": { @@ -361,9 +142,7 @@ }, "custom": { "align": "auto", - "cellOptions": { - "type": "auto" - }, + "displayMode": "auto", "inspect": false }, "mappings": [], @@ -386,12 +165,11 @@ "h": 8, "w": 12, "x": 12, - "y": 10 + "y": 1 }, - "id": 24, + "id": 13, "options": { "footer": { - "countRows": false, "fields": "", "reducer": [ "sum" @@ -400,47 +178,430 @@ }, "showHeader": true }, - "pluginVersion": "9.4.0-pre", + "pluginVersion": "9.2.0-pre", "targets": [ { "datasource": { - "type": "testdata", - "uid": "PD8C576611E62080A" + "type": "datasource", + "uid": "-- Dashboard --" }, - "rawFrameContent": "[{\r\n \"name\": \"tags\",\r\n \"fields\": [\r\n { \"name\": \"tags__time\", \"values\": [100, 101, 200] },\r\n { \"name\": \"tags__name\", \"values\": [\"v1.2\", \"v1.2b\", \"v1.3\"] }\r\n ]\r\n}]", - "refId": "tags", - "scenarioId": "raw_frame" - }, - { - "datasource": { - "type": "testdata", - "uid": "PD8C576611E62080A" - }, - "rawFrameContent": "[{\r\n \"name\": \"releases\",\r\n \"fields\": [\r\n { \"name\": \"releases__time\", \"values\": [150, 250] },\r\n { \"name\": \"releases__tag\", \"values\": [\"v1.2\", \"v1.3\"] }\r\n]}]", - "refId": "releases", - "scenarioId": "raw_frame" - }, - { - "datasource": { - "type": "testdata", - "uid": "PD8C576611E62080A" - }, - "rawFrameContent": "[{\r\n \"name\": \"features\",\r\n \"fields\": [\r\n { \"name\": \"features__name\", \"values\": [\"A\", \"B\", \"C\", \"D\", \"E\"] },\r\n { \"name\": \"features__tag\", \"values\": [\"v1.2\", \"v1.3\", \"v1.2b\", \"v1.3\", \"v1.2\"] }\r\n]}]", - "refId": "features", - "scenarioId": "raw_frame" + "panelId": 11, + "refId": "A" } ], - "title": "INNER JOIN", + "title": "Same data (as a table)", + "type": "table" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "displayMode": "auto", + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 24, + "x": 0, + "y": 9 + }, + "id": 16, + "options": { + "footer": { + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "9.2.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 11, + "refId": "A" + } + ], + "title": "OUTER join on time (default)", + "transformations": [ + { + "id": "joinByField", + "options": {} + } + ], + "type": "table" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 14 + }, + "id": 5, + "panels": [], + "title": "Join by string field", + "type": "row" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "displayMode": "auto", + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 15 + }, + "id": 2, + "options": { + "footer": { + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "frameIndex": 0, + "showHeader": true + }, + "pluginVersion": "9.2.0-pre", + "targets": [ + { + "csvContent": "OrderID,CustomerID,Time\n100,A,10000\n101,B,20000\n102,C,30000", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "Orders", + "scenarioId": "csv_content" + }, + { + "csvContent": "CustomerID,Name,Country\nA,Customer A,USA\nB,Customer B,Germany\nC,Customer C,Spain\nD,Customer D,Canada", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "hide": false, + "refId": "Customers", + "scenarioId": "csv_content" + } + ], + "title": "Orders", + "transformations": [], + "type": "table" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "displayMode": "auto", + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 15 + }, + "id": 3, + "options": { + "footer": { + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "frameIndex": 1, + "showHeader": true + }, + "pluginVersion": "9.2.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 2, + "refId": "A" + } + ], + "title": "Customers", + "transformations": [], + "type": "table" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "displayMode": "auto", + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "CustomerID" + }, + "properties": [ + { + "id": "custom.width", + "value": 101 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "OrderID" + }, + "properties": [ + { + "id": "custom.width", + "value": 89 + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 23 + }, + "id": 6, + "options": { + "footer": { + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "frameIndex": 0, + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "9.2.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 2, + "refId": "A" + } + ], + "title": "OUTER join on CustomerID (keeps missing values)", "transformations": [ { "id": "joinByField", "options": { - "fields": { - "A": "features__name", - "features": "features__tag", - "releases": "releases__tag", - "tags": "tags__name" + "byField": "CustomerID", + "mode": "outer" + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "displayMode": "auto", + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "CustomerID" }, + "properties": [ + { + "id": "custom.width", + "value": 101 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "OrderID" + }, + "properties": [ + { + "id": "custom.width", + "value": 89 + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 23 + }, + "id": 7, + "options": { + "footer": { + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "frameIndex": 0, + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "9.2.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 2, + "refId": "A" + } + ], + "title": "INNER join on CustomerID ", + "transformations": [ + { + "id": "joinByField", + "options": { + "byField": "CustomerID", "mode": "inner" } } @@ -448,8 +609,7 @@ "type": "table" } ], - "revision": 1, - "schemaVersion": 38, + "schemaVersion": 37, "style": "dark", "tags": [ "gdev", @@ -466,6 +626,6 @@ "timezone": "", "title": "Join by field", "uid": "gw0K4rmVz", - "version": 1, + "version": 6, "weekStart": "" -} \ No newline at end of file +} diff --git a/devenv/docker/blocks/auth/oauth/readme.md b/devenv/docker/blocks/auth/oauth/readme.md index 60c87d8d508..a935f2c8682 100644 --- a/devenv/docker/blocks/auth/oauth/readme.md +++ b/devenv/docker/blocks/auth/oauth/readme.md @@ -59,7 +59,7 @@ You can obtain a jwt token by using the following command for oauth-admin: ```sh curl --request POST \ - --url http://localhost:8087/auth/realms/grafana/protocol/openid-connect/token \ + --url http://localhost:8087/realms/grafana/protocol/openid-connect/token \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data client_id=grafana-oauth \ --data grant_type=password \ diff --git a/devenv/jsonnet/dev-dashboards.libsonnet b/devenv/jsonnet/dev-dashboards.libsonnet index 60a5faa1694..a8ec282659e 100644 --- a/devenv/jsonnet/dev-dashboards.libsonnet +++ b/devenv/jsonnet/dev-dashboards.libsonnet @@ -632,6 +632,13 @@ local dashboard = grafana.dashboard; id: 0, } }, + dashboard.new('timeline-thresholds-mappings', import '../dev-dashboards/panel-timeline/timeline-thresholds-mappings.json') + + resource.addMetadata('folder', 'dev-dashboards') + + { + spec+: { + id: 0, + } + }, dashboard.new('timeseries', import '../dev-dashboards/panel-timeseries/timeseries.json') + resource.addMetadata('folder', 'dev-dashboards') + { diff --git a/docs/Makefile b/docs/Makefile index 96dc60c463f..814a665d640 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -2,7 +2,7 @@ PODMAN = $(shell if command -v podman &>/dev/null; then echo podman; else echo docker; fi) IMAGE = grafana/docs-base:latest -CONTENT_PATH = /hugo/content/docs/grafana/next +CONTENT_PATH = /hugo/content/docs/grafana/latest LOCAL_STATIC_PATH = ../../website/static PORT = 3002:3002 diff --git a/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md b/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md index b2b0e284141..7240b6f960c 100644 --- a/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md +++ b/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md @@ -21,7 +21,7 @@ Watch this video to learn more about creating alerts: {{< vimeo 720001934 >}} ## Add Grafana managed rule 1. In the Grafana menu, click the **Alerting** (bell) icon to open the Alerting page listing existing alerts. -1. Click **New alert rule**. The new alerting rule page opens where the Grafana managed alerts option is selected by default. +1. Click **Create alert rule**. The new alerting rule page opens where the Grafana managed alerts option is selected by default. 1. In Step 1, add the rule name. - In **Rule name**, add a descriptive name. This name is displayed in the alert rule list. It is also the `alertname` label for every alert instance that is created from this rule. 1. In Step 2, add queries and expressions to evaluate, and then select the alert condition. diff --git a/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-recording-rule.md b/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-recording-rule.md index 4c9d6ee7e46..e7eee78396a 100644 --- a/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-recording-rule.md +++ b/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-recording-rule.md @@ -35,7 +35,7 @@ You can create and manage recording rules for an external Grafana Mimir or Loki To create a Grafana Mimir or Loki managed recording rule 1. In the Grafana menu, click the **Alerting** (bell) icon to open the Alerting page listing existing alerts. -1. Click **New alert rule**. The new alerting rule page opens where the **Grafana managed alert** option is selected by default. +1. Click **Create alert rule**. The new alerting rule page opens where the **Grafana managed alert** option is selected by default. 1. In Step 1, add the rule name. The recording name must be a Prometheus metric name and contain no whitespace. - In **Rule name**, add a descriptive name. 1. In Step 2, select **Mimir or Loki recording rule** option. @@ -49,7 +49,7 @@ To create a Grafana Mimir or Loki managed recording rule 1. Click **Save** to save the recording rule or **Save and exit** to save the recording rule and go back to the Alerting page. 1. In the Grafana menu, click the **Alerting** (bell) icon to open the Alerting page listing existing alerts. -1. Click **New alert rule**. +1. Click **Create alert rule**. 1. In Step 1, add the rule name. - In **Rule name**, add a descriptive name. This name is displayed in the alert rule list. It is also the `alertname` label for every alert instance that is created from this rule. 1. In Step 2, add the type, and storage location. diff --git a/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-rule.md b/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-rule.md index 062413e84c0..c2a4a7dd186 100644 --- a/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-rule.md +++ b/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-rule.md @@ -35,7 +35,7 @@ Watch this video to learn more about how to create a Mimir managed alert rule: { ## Add a Grafana Mimir or Loki managed alerting rule 1. In the Grafana menu, click the **Alerting** (bell) icon to open the Alerting page listing existing alerts. -1. Click **New alert rule**. The new alerting rule page opens where the Grafana managed alerts option is selected by default. +1. Click **Create alert rule**. The new alerting rule page opens where the Grafana managed alerts option is selected by default. 1. In Step 1, add the rule name. - In **Rule name**, add a descriptive name. This name is displayed in the alert rule list. It is also the `alertname` label for every alert instance that is created from this rule. 1. In Step 2, select **Mimir or Loki alert** option. diff --git a/docs/sources/alerting/fundamentals/alert-rules/message-templating.md b/docs/sources/alerting/fundamentals/alert-rules/message-templating.md index 0d43a2d948c..cd563f1e4c0 100644 --- a/docs/sources/alerting/fundamentals/alert-rules/message-templating.md +++ b/docs/sources/alerting/fundamentals/alert-rules/message-templating.md @@ -18,7 +18,7 @@ weight: 415 Notifications sent via contact points are built using notification templates. Grafana's default templates are based on the [Go templating system](https://golang.org/pkg/text/template) where some fields are evaluated as text, while others are evaluated as HTML (which can affect escaping). The default template, defined in [default_template.go](https://github.com/grafana/alerting/blob/main/alerting/notifier/channels/default_template.go), is a useful reference for custom templates. -Since most of the contact point fields can be templated, you can create reusable custom templates and use them in multiple contact points. The default template is defined in [default_template.go](https://github.com/grafana/alerting/blob/main/alerting/notifier/channels/default_template.go) which can serve as a useful reference or starting point for custom templates. +Since most of the contact point fields can be templated, you can create reusable custom templates and use them in multiple contact points. ### Using templates diff --git a/docs/sources/alerting/manage-notifications/create-contact-point.md b/docs/sources/alerting/manage-notifications/create-contact-point.md index edfcb641025..66527971770 100644 --- a/docs/sources/alerting/manage-notifications/create-contact-point.md +++ b/docs/sources/alerting/manage-notifications/create-contact-point.md @@ -24,13 +24,13 @@ Complete the following steps to add a contact point. 1. In the Grafana menu, click the **Alerting** (bell) icon to open the Alerting page listing existing alerts. 1. Click **Contact points** to open the page listing existing contact points. -1. Click **New contact point**. +1. Click **Add contact point**. 1. From the **Alertmanager** dropdown, select an Alertmanager. By default, Grafana Alertmanager is selected. 1. In **Name**, enter a descriptive name for the contact point. 1. From **Contact point integration**, select a type and fill out mandatory fields. For example, if you choose email, enter the email addresses. Or if you choose Slack, enter the Slack channel(s) and users who should be contacted. 1. Some contact point integrations, like email or webhook, have optional settings. In **Optional settings**, specify additional settings for the selected contact point integration. 1. In Notification settings, optionally select **Disable resolved message** if you do not want to be notified when an alert resolves. -1. To add another contact point integration, click **New contact point integration** and repeat steps 6 through 8. +1. To add another contact point integration, click **Add contact point integration** and repeat steps 6 through 8. 1. Click **Save contact point** to save your changes. ## Edit a contact point diff --git a/docs/sources/alerting/manage-notifications/create-silence.md b/docs/sources/alerting/manage-notifications/create-silence.md index 65adc88de92..7196427b84e 100644 --- a/docs/sources/alerting/manage-notifications/create-silence.md +++ b/docs/sources/alerting/manage-notifications/create-silence.md @@ -28,7 +28,7 @@ To add a silence, complete the following steps. 1. In the Grafana menu, click the **Alerting** (bell) icon to open the Alerting page listing existing alerts. 2. On the Alerting page, click **Silences** to open the page listing existing silences. 3. From Alertmanager drop-down, select an external Alertmanager to create and manage silences for the external data source. Otherwise, keep the default option of Grafana. -4. Click **New Silence** to open the Create silence page. +4. Click **Add Silence** to open the Create silence page. 5. In **Silence start and end**, select the start and end date to indicate when the silence should go into effect and expire. 6. Optionally, in **Duration**, specify how long the silence is enforced. This automatically updates the end time in the **Silence start and end** field. 7. In the **Name** and **Value** fields, enter one or more _Matching Labels_. Matchers determine which rules the silence will apply to. diff --git a/docs/sources/alerting/manage-notifications/template-notifications/create-notification-templates.md b/docs/sources/alerting/manage-notifications/template-notifications/create-notification-templates.md index e27166f8f2a..b9bb82984ec 100644 --- a/docs/sources/alerting/manage-notifications/template-notifications/create-notification-templates.md +++ b/docs/sources/alerting/manage-notifications/template-notifications/create-notification-templates.md @@ -24,7 +24,7 @@ In the Contact points tab, you can see a list of your notification templates. To create a template, complete the following steps. -1. Click New template. +1. Click **Add template**. 2. Choose a name for the notification template. @@ -40,7 +40,7 @@ To create a template, complete the following steps. To create a notification template that contains more than one template: -1. Click New Template. +1. Click **Add Template**. 2. Enter a name for the notification template. diff --git a/docs/sources/alerting/performance-limitations/index.md b/docs/sources/alerting/performance-limitations/index.md index e108dd1ce0c..f8e17c8715d 100644 --- a/docs/sources/alerting/performance-limitations/index.md +++ b/docs/sources/alerting/performance-limitations/index.md @@ -23,7 +23,7 @@ The following section provides a list of alerting performance considerations. - Cardinality of the rule's result set. For example, suppose you are monitoring API response errors for every API path, on every VM in your fleet. This set has a cardinality of _n_ number of paths multiplied by _v_ number of VMs. You can reduce the cardinality of a result set - perhaps by monitoring errors-per-VM instead of for each path per VM. - Complexity of the alerting query consideration. Queries that data sources can process and respond to quickly consume fewer resources. Although this consideration is less important than the other considerations listed above, if you have reduced those as much as possible, looking at individual query performance could make a difference. -Each evaluation of an alert rule generates a set of alert instances; one for each member of the result set. The state of all the instances is written to the `alert_instance` table in Grafana's SQL database. +Each evaluation of an alert rule generates a set of alert instances; one for each member of the result set. The state of all the instances is written to the `alert_instance` table in Grafana's SQL database. This number of write-heavy operations can cause issues when using SQLite. Grafana Alerting exposes a metric, `grafana_alerting_rule_evaluations_total` that counts the number of alert rule evaluations. To get a feel for the influence of rule evaluations on your Grafana instance, you can observe the rate of evaluations and compare it with resource consumption. In a Prometheus-compatible database, you can use the query `rate(grafana_alerting_rule_evaluations_total[5m])` to compute the rate over 5 minute windows of time. It's important to remember that this isn't the full picture of rule evaluation. For example, the load will be unevenly distributed if you have some rules that evaluate every 10 seconds, and others every 30 minutes. diff --git a/docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md b/docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md index 1aec13bc0e4..82b1c05ae20 100644 --- a/docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md +++ b/docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md @@ -22,20 +22,20 @@ Details on how to set up the files and which fields are required for each object **Note:** -Provisioning takes place during the initial set up of your Grafana system, but you can re-run it at any time using the [Grafana Alerting provisioning API](https://grafana.com/docs/grafana/latest/developers/http_api/admin/#reload-provisioning-configurations). +Provisioning takes place during the initial set up of your Grafana system, but you can re-run it at any time using the [Grafana Admin API](https://grafana.com/docs/grafana/latest/developers/http_api/admin/#reload-provisioning-configurations). ### Provision alert rules Create or delete alert rules in your Grafana instance(s). -1. Create an alert rule in Grafana. -1. Use the [Alerting provisioning API](https://grafana.com/docs/grafana/latest/developers/http_api/alerting_provisioning/#route-get-alert-rule) to extract the alert rule. +1. Create alert rules in Grafana. +1. Use the [Alerting provisioning API](https://grafana.com/docs/grafana/latest/developers/http_api/alerting_provisioning/#route-get-alert-rule-export) export endpoints to download a provisioning file for your alert rules. 1. Copy the contents into a YAML or JSON configuration file in the default provisioning directory or in your configured directory. Example configuration files can be found below. 1. Ensure that your files are in the right directory on the node running the Grafana server, so that they deploy alongside your Grafana instance(s). -1. Delete the alert rule in Grafana. +1. Delete the alert rules in Grafana that will be provisioned. **Note:** diff --git a/docs/sources/dashboards/create-reports/index.md b/docs/sources/dashboards/create-reports/index.md index 2ddd5e6673e..7def573355e 100644 --- a/docs/sources/dashboards/create-reports/index.md +++ b/docs/sources/dashboards/create-reports/index.md @@ -181,7 +181,7 @@ You can send a report email with an image of the dashboard embedded in the email You can generate and save PDF files of any dashboard. -> **Note:** Available in [Grafana Enterprise]({{< relref "../../introduction/grafana-enterprise/" >}}). +> **Note:** Available in [Grafana Enterprise]({{< relref "../../introduction/grafana-enterprise/" >}}) version 6.7 and later, and [Grafana Cloud Pro and Advanced](/docs/grafana-cloud/). 1. In the upper-right corner of the dashboard that you want to export as PDF, click the **Share dashboard** icon. 1. On the PDF tab, select a layout option for the exported dashboard: **Portrait** or **Landscape**. diff --git a/docs/sources/datasources/aws-cloudwatch/_index.md b/docs/sources/datasources/aws-cloudwatch/_index.md index 9fc5edc9bd3..2df89e86dc5 100644 --- a/docs/sources/datasources/aws-cloudwatch/_index.md +++ b/docs/sources/datasources/aws-cloudwatch/_index.md @@ -167,7 +167,7 @@ You can attach these permissions to the IAM role or IAM user you configured in [ } ``` -**Cross-account observability:** +**Cross-account observability: (see below) ** ```json { @@ -369,3 +369,9 @@ If you use multiple regions or configured more than one CloudWatch data source t To request a quota increase, visit the [AWS Service Quotas console](https://console.aws.amazon.com/servicequotas/home?r#!/services/monitoring/quotas/L-5E141212). For more information, refer to the AWS documentation for [Service Quotas](https://docs.aws.amazon.com/servicequotas/latest/userguide/intro.html) and [CloudWatch limits](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_limits.html). + +## Cross-account observability + +The CloudWatch plugin enables you to monitor and troubleshoot applications across multiple regional accounts. Using cross-account observability, you can seamlessly search, visualize and analyze metrics and logs without worrying about account boundaries. + +To use this feature, configure in the [AWS console under Cloudwatch Settings](https://aws.amazon.com/blogs/aws/new-amazon-cloudwatch-cross-account-observability/), a monitoring and source account, and then add the necessary IAM permissions as described above. diff --git a/docs/sources/datasources/aws-cloudwatch/query-editor/index.md b/docs/sources/datasources/aws-cloudwatch/query-editor/index.md index fe91bc93cbc..37d85968eca 100644 --- a/docs/sources/datasources/aws-cloudwatch/query-editor/index.md +++ b/docs/sources/datasources/aws-cloudwatch/query-editor/index.md @@ -218,15 +218,6 @@ When making `stats` queries in [Explore]({{< relref "../../../explore/" >}}), ma {{< figure src="/static/img/docs/v70/explore-mode-switcher.png" max-width="500px" class="docs-image--right" caption="Explore mode switcher" >}} -## Cross-account observability - -The CloudWatch plugin provides the ability to monitor and troubleshoot applications that span across multiple accounts within a region. Using cross-account observability, you can seamlessly search, visualize and analyze metrics and logs, without having to worry about account boundaries. - -> **Note:** This feature is currently behind the `cloudWatchCrossAccountQuerying` feature toggle. - -> You can enable feature toggles through configuration file or environment variables. See configuration [docs]({{< relref "../../../setup-grafana/configure-grafana/#feature_toggles" >}}) for details. -> Grafana Cloud users can access this feature by [opening a support ticket in the Cloud Portal](https://grafana.com/profile/org#support). - ### Getting started To enable cross-account observability, first enable it in CloudWatch using the official [CloudWatch docs](http://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-Unified-Cross-Account.html), then add [two new API actions]({{< relref "../#cross-account-observability" >}}) to the IAM policy attached to the role/user running the plugin. diff --git a/docs/sources/datasources/jaeger/_index.md b/docs/sources/datasources/jaeger/_index.md index 4441dc5fc61..0d2bcb14636 100644 --- a/docs/sources/datasources/jaeger/_index.md +++ b/docs/sources/datasources/jaeger/_index.md @@ -154,7 +154,7 @@ datasources: tags: [{ key: 'service.name', value: 'service' }, { key: 'job' }] queries: - name: 'Sample query' - query: 'sum(rate(tempo_spanmetrics_latency_bucket{$__tags}[5m]))' + query: 'sum(rate(traces_spanmetrics_latency_bucket{$__tags}[5m]))' secureJsonData: basicAuthPassword: my_password ``` diff --git a/docs/sources/datasources/tempo/_index.md b/docs/sources/datasources/tempo/_index.md index 124e273f6c3..419f4f78e1c 100644 --- a/docs/sources/datasources/tempo/_index.md +++ b/docs/sources/datasources/tempo/_index.md @@ -175,7 +175,7 @@ datasources: tags: [{ key: 'service.name', value: 'service' }, { key: 'job' }] queries: - name: 'Sample query' - query: 'sum(rate(tempo_spanmetrics_latency_bucket{$__tags}[5m]))' + query: 'sum(rate(traces_spanmetrics_latency_bucket{$__tags}[5m]))' serviceMap: datasourceUid: 'prometheus' search: diff --git a/docs/sources/developers/http_api/alerting_provisioning.md b/docs/sources/developers/http_api/alerting_provisioning.md index 1198c17eead..98ff5686667 100644 --- a/docs/sources/developers/http_api/alerting_provisioning.md +++ b/docs/sources/developers/http_api/alerting_provisioning.md @@ -18,7 +18,7 @@ title: 'Alerting Provisioning HTTP API ' ### Version -1.0.0 +1.1.0 ## Content negotiation @@ -29,53 +29,61 @@ title: 'Alerting Provisioning HTTP API ' ### Produces - application/json +- text/yaml +- application/yaml ## All endpoints ### Alert rules -| Method | URI | Name | Summary | -| ------ | ----------------------------------------------------------- | --------------------------------------------------------- | ------------------------------------ | -| GET | /api/v1/provisioning/alert-rules/{UID} | [route get alert rule](#route-get-alert-rule) | Get a specific alert rule by UID. | -| POST | /api/v1/provisioning/alert-rules | [route post alert rule](#route-post-alert-rule) | Create a new alert rule. | -| PUT | /api/v1/provisioning/alert-rules/{UID} | [route put alert rule](#route-put-alert-rule) | Update an existing alert rule. | -| PUT | /api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group} | [route put alert rule group](#route-put-alert-rule-group) | Update the interval of a rule group. | -| DELETE | /api/v1/provisioning/alert-rules/{UID} | [route delete alert rule](#route-delete-alert-rule) | Delete a specific alert rule by UID. | +| Method | URI | Name | Summary | +| ------ | ------------------------------------------------------------------ | ----------------------------------------------------------------------- | ------------------------------------------------------- | +| DELETE | /api/v1/provisioning/alert-rules/{UID} | [route delete alert rule](#route-delete-alert-rule) | Delete a specific alert rule by UID. | +| GET | /api/v1/provisioning/alert-rules/{UID} | [route get alert rule](#route-get-alert-rule) | Get a specific alert rule by UID. | +| GET | /api/v1/provisioning/alert-rules/{UID}/export | [route get alert rule export](#route-get-alert-rule-export) | Export an alert rule in provisioning file format. | +| GET | /api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group} | [route get alert rule group](#route-get-alert-rule-group) | Get a rule group. | +| GET | /api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}/export | [route get alert rule group export](#route-get-alert-rule-group-export) | Export an alert rule group in provisioning file format. | +| GET | /api/v1/provisioning/alert-rules | [route get alert rules](#route-get-alert-rules) | Get all the alert rules. | +| GET | /api/v1/provisioning/alert-rules/export | [route get alert rules export](#route-get-alert-rules-export) | Export all alert rules in provisioning file format. | +| POST | /api/v1/provisioning/alert-rules | [route post alert rule](#route-post-alert-rule) | Create a new alert rule. | +| PUT | /api/v1/provisioning/alert-rules/{UID} | [route put alert rule](#route-put-alert-rule) | Update an existing alert rule. | +| PUT | /api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group} | [route put alert rule group](#route-put-alert-rule-group) | Update the interval of a rule group. | ### Contact points | Method | URI | Name | Summary | | ------ | ----------------------------------------- | --------------------------------------------------------- | --------------------------------- | +| DELETE | /api/v1/provisioning/contact-points/{UID} | [route delete contactpoints](#route-delete-contactpoints) | Delete a contact point. | | GET | /api/v1/provisioning/contact-points | [route get contactpoints](#route-get-contactpoints) | Get all the contact points. | | POST | /api/v1/provisioning/contact-points | [route post contactpoints](#route-post-contactpoints) | Create a contact point. | | PUT | /api/v1/provisioning/contact-points/{UID} | [route put contactpoint](#route-put-contactpoint) | Update an existing contact point. | -| DELETE | /api/v1/provisioning/contact-points/{UID} | [route delete contactpoints](#route-delete-contactpoints) | Delete a contact point. | ### Notification policies -| Method | URI | Name | Summary | -| ------ | ----------------------------- | ----------------------------------------------- | ---------------------------------- | -| GET | /api/v1/provisioning/policies | [route get policy tree](#route-get-policy-tree) | Get the notification policy tree. | -| PUT | /api/v1/provisioning/policies | [route put policy tree](#route-put-policy-tree) | Sets the notification policy tree. | +| Method | URI | Name | Summary | +| ------ | ----------------------------- | --------------------------------------------------- | ------------------------------------ | +| DELETE | /api/v1/provisioning/policies | [route reset policy tree](#route-reset-policy-tree) | Clears the notification policy tree. | +| GET | /api/v1/provisioning/policies | [route get policy tree](#route-get-policy-tree) | Get the notification policy tree. | +| PUT | /api/v1/provisioning/policies | [route put policy tree](#route-put-policy-tree) | Sets the notification policy tree. | ### Mute timings | Method | URI | Name | Summary | | ------ | ---------------------------------------- | ----------------------------------------------------- | -------------------------------- | -| GET | /api/v1/provisioning/mute-timings | [route get mute timings](#route-get-mute-timings) | Get all the mute timings. | +| DELETE | /api/v1/provisioning/mute-timings/{name} | [route delete mute timing](#route-delete-mute-timing) | Delete a mute timing. | | GET | /api/v1/provisioning/mute-timings/{name} | [route get mute timing](#route-get-mute-timing) | Get a mute timing. | +| GET | /api/v1/provisioning/mute-timings | [route get mute timings](#route-get-mute-timings) | Get all the mute timings. | | POST | /api/v1/provisioning/mute-timings | [route post mute timing](#route-post-mute-timing) | Create a new mute timing. | | PUT | /api/v1/provisioning/mute-timings/{name} | [route put mute timing](#route-put-mute-timing) | Replace an existing mute timing. | -| DELETE | /api/v1/provisioning/mute-timings/{name} | [route delete mute timing](#route-delete-mute-timing) | Delete a mute timing. | ### Templates -| Method | URI | Name | Summary | -| ------ | ------------------------------------- | ----------------------------------------------- | ------------------------------- | -| GET | /api/v1/provisioning/templates | [route get templates](#route-get-templates) | Get all notification templates. | -| GET | /api/v1/provisioning/templates/{name} | [route get template](#route-get-template) | Get a notification template. | -| PUT | /api/v1/provisioning/templates/{name} | [route put template](#route-put-template) | Creates or updates a template. | -| DELETE | /api/v1/provisioning/templates/{name} | [route delete template](#route-delete-template) | Delete a template. | +| Method | URI | Name | Summary | +| ------ | ------------------------------------- | ----------------------------------------------- | ------------------------------------------ | +| DELETE | /api/v1/provisioning/templates/{name} | [route delete template](#route-delete-template) | Delete a template. | +| GET | /api/v1/provisioning/templates/{name} | [route get template](#route-get-template) | Get a notification template. | +| GET | /api/v1/provisioning/templates | [route get templates](#route-get-templates) | Get all notification templates. | +| PUT | /api/v1/provisioning/templates/{name} | [route put template](#route-put-template) | Updates an existing notification template. | ## Paths @@ -87,16 +95,15 @@ DELETE /api/v1/provisioning/alert-rules/{UID} #### Parameters -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| ---- | ------ | ------ | -------- | --------- | :------: | ------- | ----------- | -| UID | `path` | string | `string` | | ✓ | | | +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| ---- | ------ | ------ | -------- | --------- | :------: | ------- | -------------- | +| UID | `path` | string | `string` | | ✓ | | Alert rule UID | #### All responses -| Code | Status | Description | Has headers | Schema | -| ----------------------------------- | ----------- | ---------------------------------------- | :---------: | --------------------------------------------- | -| [204](#route-delete-alert-rule-204) | No Content | The alert rule was deleted successfully. | | [schema](#route-delete-alert-rule-204-schema) | -| [400](#route-delete-alert-rule-400) | Bad Request | ValidationError | | [schema](#route-delete-alert-rule-400-schema) | +| Code | Status | Description | Has headers | Schema | +| ----------------------------------- | ---------- | ---------------------------------------- | :---------: | --------------------------------------------- | +| [204](#route-delete-alert-rule-204) | No Content | The alert rule was deleted successfully. | | [schema](#route-delete-alert-rule-204-schema) | #### Responses @@ -106,14 +113,6 @@ Status: No Content ###### Schema -##### 400 - ValidationError - -Status: Bad Request - -###### Schema - -[ValidationError](#validation-error) - ### Delete a contact point. (_RouteDeleteContactpoints_) ``` @@ -126,34 +125,23 @@ DELETE /api/v1/provisioning/contact-points/{UID} #### Parameters -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| ---- | ------ | ------ | -------- | --------- | :------: | ------- | ------------------------------------------------- | -| UID | `path` | string | `string` | | ✓ | | UID should be the contact point unique identifier | +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| ---- | ------ | ------ | -------- | --------- | :------: | ------- | ------------------------------------------ | +| UID | `path` | string | `string` | | ✓ | | UID is the contact point unique identifier | #### All responses -| Code | Status | Description | Has headers | Schema | -| -------------------------------------- | ----------- | --------------- | :---------: | ------------------------------------------------ | -| [202](#route-delete-contactpoints-202) | Accepted | Ack | | [schema](#route-delete-contactpoints-202-schema) | -| [400](#route-delete-contactpoints-400) | Bad Request | ValidationError | | [schema](#route-delete-contactpoints-400-schema) | +| Code | Status | Description | Has headers | Schema | +| -------------------------------------- | ---------- | ------------------------------------------- | :---------: | ------------------------------------------------ | +| [204](#route-delete-contactpoints-204) | No Content | The contact point was deleted successfully. | | [schema](#route-delete-contactpoints-204-schema) | #### Responses -##### 202 - Ack +##### 204 - The contact point was deleted successfully. -Status: Accepted +Status: No Content -###### Schema - -[Ack](#ack) - -##### 400 - ValidationError - -Status: Bad Request - -###### Schema - -[ValidationError](#validation-error) +###### Schema ### Delete a mute timing. (_RouteDeleteMuteTiming_) @@ -163,26 +151,24 @@ DELETE /api/v1/provisioning/mute-timings/{name} #### Parameters -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| ---- | ------ | ------ | -------- | --------- | :------: | ------- | ------------- | -| name | `path` | string | `string` | | ✓ | | Template Name | +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| ---- | ------ | ------ | -------- | --------- | :------: | ------- | ---------------- | +| name | `path` | string | `string` | | ✓ | | Mute timing name | #### All responses -| Code | Status | Description | Has headers | Schema | -| ------------------------------------ | ---------- | ----------- | :---------: | ---------------------------------------------- | -| [204](#route-delete-mute-timing-204) | No Content | Ack | | [schema](#route-delete-mute-timing-204-schema) | +| Code | Status | Description | Has headers | Schema | +| ------------------------------------ | ---------- | ----------------------------------------- | :---------: | ---------------------------------------------- | +| [204](#route-delete-mute-timing-204) | No Content | The mute timing was deleted successfully. | | [schema](#route-delete-mute-timing-204-schema) | #### Responses -##### 204 - Ack +##### 204 - The mute timing was deleted successfully. Status: No Content ###### Schema -[Ack](#ack) - ### Delete a template. (_RouteDeleteTemplate_) ``` @@ -197,20 +183,18 @@ DELETE /api/v1/provisioning/templates/{name} #### All responses -| Code | Status | Description | Has headers | Schema | -| --------------------------------- | ---------- | ----------- | :---------: | ------------------------------------------- | -| [204](#route-delete-template-204) | No Content | Ack | | [schema](#route-delete-template-204-schema) | +| Code | Status | Description | Has headers | Schema | +| --------------------------------- | ---------- | -------------------------------------- | :---------: | ------------------------------------------- | +| [204](#route-delete-template-204) | No Content | The template was deleted successfully. | | [schema](#route-delete-template-204-schema) | #### Responses -##### 204 - Ack +##### 204 - The template was deleted successfully. Status: No Content ###### Schema -[Ack](#ack) - ### Get a specific alert rule by UID. (_RouteGetAlertRule_) ``` @@ -219,34 +203,210 @@ GET /api/v1/provisioning/alert-rules/{UID} #### Parameters -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| ---- | ------ | ------ | -------- | --------- | :------: | ------- | ----------- | -| UID | `path` | string | `string` | | ✓ | | | +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| ---- | ------ | ------ | -------- | --------- | :------: | ------- | -------------- | +| UID | `path` | string | `string` | | ✓ | | Alert rule UID | #### All responses -| Code | Status | Description | Has headers | Schema | -| -------------------------------- | ----------- | --------------- | :---------: | ------------------------------------------ | -| [200](#route-get-alert-rule-200) | OK | AlertRule | | [schema](#route-get-alert-rule-200-schema) | -| [400](#route-get-alert-rule-400) | Bad Request | ValidationError | | [schema](#route-get-alert-rule-400-schema) | +| Code | Status | Description | Has headers | Schema | +| -------------------------------- | --------- | -------------------- | :---------: | ------------------------------------------ | +| [200](#route-get-alert-rule-200) | OK | ProvisionedAlertRule | | [schema](#route-get-alert-rule-200-schema) | +| [404](#route-get-alert-rule-404) | Not Found | Not found. | | [schema](#route-get-alert-rule-404-schema) | #### Responses -##### 200 - AlertRule +##### 200 - ProvisionedAlertRule Status: OK ###### Schema -[AlertRule](#alert-rule) +[ProvisionedAlertRule](#provisioned-alert-rule) -##### 400 - ValidationError +##### 404 - Not found. -Status: Bad Request +Status: Not Found -###### Schema +###### Schema -[ValidationError](#validation-error) +### Export an alert rule in provisioning file format. (_RouteGetAlertRuleExport_) + +``` +GET /api/v1/provisioning/alert-rules/{UID}/export +``` + +#### Produces + +- application/json +- application/yaml +- text/yaml + +#### Parameters + +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| -------- | ------- | ------- | -------- | --------- | :------: | ------- | -------------------------------------------------- | +| UID | `path` | string | `string` | | ✓ | | Alert rule UID | +| download | `query` | boolean | `bool` | | | | Whether to initiate a download of the file or not. | + +#### All responses + +| Code | Status | Description | Has headers | Schema | +| --------------------------------------- | --------- | ------------------ | :---------: | ------------------------------------------------- | +| [200](#route-get-alert-rule-export-200) | OK | AlertingFileExport | | [schema](#route-get-alert-rule-export-200-schema) | +| [404](#route-get-alert-rule-export-404) | Not Found | Not found. | | [schema](#route-get-alert-rule-export-404-schema) | + +#### Responses + +##### 200 - AlertingFileExport + +Status: OK + +###### Schema + +[AlertingFileExport](#alerting-file-export) + +##### 404 - Not found. + +Status: Not Found + +###### Schema + +### Get a rule group. (_RouteGetAlertRuleGroup_) + +``` +GET /api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group} +``` + +#### Parameters + +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| --------- | ------ | ------ | -------- | --------- | :------: | ------- | ----------- | +| FolderUID | `path` | string | `string` | | ✓ | | | +| Group | `path` | string | `string` | | ✓ | | | + +#### All responses + +| Code | Status | Description | Has headers | Schema | +| -------------------------------------- | --------- | -------------- | :---------: | ------------------------------------------------ | +| [200](#route-get-alert-rule-group-200) | OK | AlertRuleGroup | | [schema](#route-get-alert-rule-group-200-schema) | +| [404](#route-get-alert-rule-group-404) | Not Found | Not found. | | [schema](#route-get-alert-rule-group-404-schema) | + +#### Responses + +##### 200 - AlertRuleGroup + +Status: OK + +###### Schema + +[AlertRuleGroup](#alert-rule-group) + +##### 404 - Not found. + +Status: Not Found + +###### Schema + +### Export an alert rule group in provisioning file format. (_RouteGetAlertRuleGroupExport_) + +``` +GET /api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}/export +``` + +#### Produces + +- application/json +- application/yaml +- text/yaml + +#### Parameters + +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| --------- | ------- | ------- | -------- | --------- | :------: | ------- | -------------------------------------------------- | +| FolderUID | `path` | string | `string` | | ✓ | | | +| Group | `path` | string | `string` | | ✓ | | | +| download | `query` | boolean | `bool` | | | | Whether to initiate a download of the file or not. | + +#### All responses + +| Code | Status | Description | Has headers | Schema | +| --------------------------------------------- | --------- | ------------------ | :---------: | ------------------------------------------------------- | +| [200](#route-get-alert-rule-group-export-200) | OK | AlertingFileExport | | [schema](#route-get-alert-rule-group-export-200-schema) | +| [404](#route-get-alert-rule-group-export-404) | Not Found | Not found. | | [schema](#route-get-alert-rule-group-export-404-schema) | + +#### Responses + +##### 200 - AlertingFileExport + +Status: OK + +###### Schema + +[AlertingFileExport](#alerting-file-export) + +##### 404 - Not found. + +Status: Not Found + +###### Schema + +### Get all the alert rules. (_RouteGetAlertRules_) + +``` +GET /api/v1/provisioning/alert-rules +``` + +#### All responses + +| Code | Status | Description | Has headers | Schema | +| --------------------------------- | ------ | --------------------- | :---------: | ------------------------------------------- | +| [200](#route-get-alert-rules-200) | OK | ProvisionedAlertRules | | [schema](#route-get-alert-rules-200-schema) | + +#### Responses + +##### 200 - ProvisionedAlertRules + +Status: OK + +###### Schema + +[ProvisionedAlertRules](#provisioned-alert-rules) + +### Export all alert rules in provisioning file format. (_RouteGetAlertRulesExport_) + +``` +GET /api/v1/provisioning/alert-rules/export +``` + +#### Parameters + +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| -------- | ------- | ------- | ------- | --------- | :------: | ------- | -------------------------------------------------- | +| download | `query` | boolean | `bool` | | | | Whether to initiate a download of the file or not. | + +#### All responses + +| Code | Status | Description | Has headers | Schema | +| ---------------------------------------- | --------- | ------------------ | :---------: | -------------------------------------------------- | +| [200](#route-get-alert-rules-export-200) | OK | AlertingFileExport | | [schema](#route-get-alert-rules-export-200-schema) | +| [404](#route-get-alert-rules-export-404) | Not Found | Not found. | | [schema](#route-get-alert-rules-export-404-schema) | + +#### Responses + +##### 200 - AlertingFileExport + +Status: OK + +###### Schema + +[AlertingFileExport](#alerting-file-export) + +##### 404 - Not found. + +Status: Not Found + +###### Schema ### Get all the contact points. (_RouteGetContactpoints_) @@ -254,30 +414,27 @@ Status: Bad Request GET /api/v1/provisioning/contact-points ``` +#### Parameters + +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| ---- | ------- | ------ | -------- | --------- | :------: | ------- | -------------- | +| name | `query` | string | `string` | | | | Filter by name | + #### All responses -| Code | Status | Description | Has headers | Schema | -| ----------------------------------- | ----------- | --------------- | :---------: | --------------------------------------------- | -| [200](#route-get-contactpoints-200) | OK | Route | | [schema](#route-get-contactpoints-200-schema) | -| [400](#route-get-contactpoints-400) | Bad Request | ValidationError | | [schema](#route-get-contactpoints-400-schema) | +| Code | Status | Description | Has headers | Schema | +| ----------------------------------- | ------ | ------------- | :---------: | --------------------------------------------- | +| [200](#route-get-contactpoints-200) | OK | ContactPoints | | [schema](#route-get-contactpoints-200-schema) | #### Responses -##### 200 - Route +##### 200 - ContactPoints Status: OK ###### Schema -[Route](#route) - -##### 400 - ValidationError - -Status: Bad Request - -###### Schema - -[ValidationError](#validation-error) +[ContactPoints](#contact-points) ### Get a mute timing. (_RouteGetMuteTiming_) @@ -287,16 +444,16 @@ GET /api/v1/provisioning/mute-timings/{name} #### Parameters -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| ---- | ------ | ------ | -------- | --------- | :------: | ------- | ------------- | -| name | `path` | string | `string` | | ✓ | | Template Name | +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| ---- | ------ | ------ | -------- | --------- | :------: | ------- | ---------------- | +| name | `path` | string | `string` | | ✓ | | Mute timing name | #### All responses -| Code | Status | Description | Has headers | Schema | -| --------------------------------- | ----------- | ---------------- | :---------: | ------------------------------------------- | -| [200](#route-get-mute-timing-200) | OK | MuteTimeInterval | | [schema](#route-get-mute-timing-200-schema) | -| [400](#route-get-mute-timing-400) | Bad Request | ValidationError | | [schema](#route-get-mute-timing-400-schema) | +| Code | Status | Description | Has headers | Schema | +| --------------------------------- | --------- | ---------------- | :---------: | ------------------------------------------- | +| [200](#route-get-mute-timing-200) | OK | MuteTimeInterval | | [schema](#route-get-mute-timing-200-schema) | +| [404](#route-get-mute-timing-404) | Not Found | Not found. | | [schema](#route-get-mute-timing-404-schema) | #### Responses @@ -308,13 +465,11 @@ Status: OK [MuteTimeInterval](#mute-time-interval) -##### 400 - ValidationError +##### 404 - Not found. -Status: Bad Request +Status: Not Found -###### Schema - -[ValidationError](#validation-error) +###### Schema ### Get all the mute timings. (_RouteGetMuteTimings_) @@ -324,10 +479,9 @@ GET /api/v1/provisioning/mute-timings #### All responses -| Code | Status | Description | Has headers | Schema | -| ---------------------------------- | ----------- | --------------- | :---------: | -------------------------------------------- | -| [200](#route-get-mute-timings-200) | OK | MuteTimings | | [schema](#route-get-mute-timings-200-schema) | -| [400](#route-get-mute-timings-400) | Bad Request | ValidationError | | [schema](#route-get-mute-timings-400-schema) | +| Code | Status | Description | Has headers | Schema | +| ---------------------------------- | ------ | ----------- | :---------: | -------------------------------------------- | +| [200](#route-get-mute-timings-200) | OK | MuteTimings | | [schema](#route-get-mute-timings-200-schema) | #### Responses @@ -339,14 +493,6 @@ Status: OK [MuteTimings](#mute-timings) -##### 400 - ValidationError - -Status: Bad Request - -###### Schema - -[ValidationError](#validation-error) - ### Get the notification policy tree. (_RouteGetPolicyTree_) ``` @@ -355,10 +501,9 @@ GET /api/v1/provisioning/policies #### All responses -| Code | Status | Description | Has headers | Schema | -| --------------------------------- | ----------- | --------------- | :---------: | ------------------------------------------- | -| [200](#route-get-policy-tree-200) | OK | Route | | [schema](#route-get-policy-tree-200-schema) | -| [400](#route-get-policy-tree-400) | Bad Request | ValidationError | | [schema](#route-get-policy-tree-400-schema) | +| Code | Status | Description | Has headers | Schema | +| --------------------------------- | ------ | ----------- | :---------: | ------------------------------------------- | +| [200](#route-get-policy-tree-200) | OK | Route | | [schema](#route-get-policy-tree-200-schema) | #### Responses @@ -370,14 +515,6 @@ Status: OK [Route](#route) -##### 400 - ValidationError - -Status: Bad Request - -###### Schema - -[ValidationError](#validation-error) - ### Get a notification template. (_RouteGetTemplate_) ``` @@ -395,7 +532,7 @@ GET /api/v1/provisioning/templates/{name} | Code | Status | Description | Has headers | Schema | | ------------------------------ | --------- | -------------------- | :---------: | ---------------------------------------- | | [200](#route-get-template-200) | OK | NotificationTemplate | | [schema](#route-get-template-200-schema) | -| [404](#route-get-template-404) | Not Found | NotFound | | [schema](#route-get-template-404-schema) | +| [404](#route-get-template-404) | Not Found | Not found. | | [schema](#route-get-template-404-schema) | #### Responses @@ -405,16 +542,14 @@ Status: OK ###### Schema -[NotificationTemplate](#message-template) +[NotificationTemplate](#notification-template) -##### 404 - NotFound +##### 404 - Not found. Status: Not Found ###### Schema -[NotFound](#not-found) - ### Get all notification templates. (_RouteGetTemplates_) ``` @@ -423,28 +558,26 @@ GET /api/v1/provisioning/templates #### All responses -| Code | Status | Description | Has headers | Schema | -| ------------------------------- | ----------- | -------------------- | :---------: | ----------------------------------------- | -| [200](#route-get-templates-200) | OK | NotificationTemplate | | [schema](#route-get-templates-200-schema) | -| [400](#route-get-templates-400) | Bad Request | ValidationError | | [schema](#route-get-templates-400-schema) | +| Code | Status | Description | Has headers | Schema | +| ------------------------------- | --------- | --------------------- | :---------: | ----------------------------------------- | +| [200](#route-get-templates-200) | OK | NotificationTemplates | | [schema](#route-get-templates-200-schema) | +| [404](#route-get-templates-404) | Not Found | Not found. | | [schema](#route-get-templates-404-schema) | #### Responses -##### 200 - NotificationTemplate +##### 200 - NotificationTemplates Status: OK ###### Schema -[NotificationTemplate](#message-template) +[NotificationTemplates](#notification-templates) -##### 400 - ValidationError +##### 404 - Not found. -Status: Bad Request +Status: Not Found -###### Schema - -[ValidationError](#validation-error) +###### Schema ### Create a new alert rule. (_RoutePostAlertRule_) @@ -452,28 +585,33 @@ Status: Bad Request POST /api/v1/provisioning/alert-rules ``` +#### Consumes + +- application/json + #### Parameters -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| ---- | ------ | ------------------------ | ------------------ | --------- | :------: | ------- | ----------- | -| Body | `body` | [AlertRule](#alert-rule) | `models.AlertRule` | | | | | +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| -------------------- | -------- | ----------------------------------------------- | ----------------------------- | --------- | :------: | ------- | ----------- | +| X-Disable-Provenance | `header` | string | `string` | | | | | +| Body | `body` | [ProvisionedAlertRule](#provisioned-alert-rule) | `models.ProvisionedAlertRule` | | | | | #### All responses -| Code | Status | Description | Has headers | Schema | -| --------------------------------- | ----------- | --------------- | :---------: | ------------------------------------------- | -| [201](#route-post-alert-rule-201) | Created | AlertRule | | [schema](#route-post-alert-rule-201-schema) | -| [400](#route-post-alert-rule-400) | Bad Request | ValidationError | | [schema](#route-post-alert-rule-400-schema) | +| Code | Status | Description | Has headers | Schema | +| --------------------------------- | ----------- | -------------------- | :---------: | ------------------------------------------- | +| [201](#route-post-alert-rule-201) | Created | ProvisionedAlertRule | | [schema](#route-post-alert-rule-201-schema) | +| [400](#route-post-alert-rule-400) | Bad Request | ValidationError | | [schema](#route-post-alert-rule-400-schema) | #### Responses -##### 201 - AlertRule +##### 201 - ProvisionedAlertRule Status: Created ###### Schema -[AlertRule](#alert-rule) +[ProvisionedAlertRule](#provisioned-alert-rule) ##### 400 - ValidationError @@ -501,20 +639,20 @@ POST /api/v1/provisioning/contact-points #### All responses -| Code | Status | Description | Has headers | Schema | -| ------------------------------------ | ----------- | --------------- | :---------: | ---------------------------------------------- | -| [202](#route-post-contactpoints-202) | Accepted | Ack | | [schema](#route-post-contactpoints-202-schema) | -| [400](#route-post-contactpoints-400) | Bad Request | ValidationError | | [schema](#route-post-contactpoints-400-schema) | +| Code | Status | Description | Has headers | Schema | +| ------------------------------------ | ----------- | -------------------- | :---------: | ---------------------------------------------- | +| [202](#route-post-contactpoints-202) | Accepted | EmbeddedContactPoint | | [schema](#route-post-contactpoints-202-schema) | +| [400](#route-post-contactpoints-400) | Bad Request | ValidationError | | [schema](#route-post-contactpoints-400-schema) | #### Responses -##### 202 - Ack +##### 202 - EmbeddedContactPoint Status: Accepted ###### Schema -[Ack](#ack) +[EmbeddedContactPoint](#embedded-contact-point) ##### 400 - ValidationError @@ -577,27 +715,28 @@ PUT /api/v1/provisioning/alert-rules/{UID} #### Parameters -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| ---- | ------ | ------------------------ | ------------------ | --------- | :------: | ------- | ----------- | -| UID | `path` | string | `string` | | ✓ | | | -| Body | `body` | [AlertRule](#alert-rule) | `models.AlertRule` | | | | | +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| -------------------- | -------- | ----------------------------------------------- | ----------------------------- | --------- | :------: | ------- | -------------- | +| UID | `path` | string | `string` | | ✓ | | Alert rule UID | +| X-Disable-Provenance | `header` | string | `string` | | | | | +| Body | `body` | [ProvisionedAlertRule](#provisioned-alert-rule) | `models.ProvisionedAlertRule` | | | | | #### All responses -| Code | Status | Description | Has headers | Schema | -| -------------------------------- | ----------- | --------------- | :---------: | ------------------------------------------ | -| [200](#route-put-alert-rule-200) | OK | AlertRule | | [schema](#route-put-alert-rule-200-schema) | -| [400](#route-put-alert-rule-400) | Bad Request | ValidationError | | [schema](#route-put-alert-rule-400-schema) | +| Code | Status | Description | Has headers | Schema | +| -------------------------------- | ----------- | -------------------- | :---------: | ------------------------------------------ | +| [200](#route-put-alert-rule-200) | OK | ProvisionedAlertRule | | [schema](#route-put-alert-rule-200-schema) | +| [400](#route-put-alert-rule-400) | Bad Request | ValidationError | | [schema](#route-put-alert-rule-400-schema) | #### Responses -##### 200 - AlertRule +##### 200 - ProvisionedAlertRule Status: OK ###### Schema -[AlertRule](#alert-rule) +[ProvisionedAlertRule](#provisioned-alert-rule) ##### 400 - ValidationError @@ -662,10 +801,10 @@ PUT /api/v1/provisioning/contact-points/{UID} #### Parameters -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| ---- | ------ | ----------------------------------------------- | ----------------------------- | --------- | :------: | ------- | ------------------------------------------------- | -| UID | `path` | string | `string` | | ✓ | | UID should be the contact point unique identifier | -| Body | `body` | [EmbeddedContactPoint](#embedded-contact-point) | `models.EmbeddedContactPoint` | | | | | +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| ---- | ------ | ----------------------------------------------- | ----------------------------- | --------- | :------: | ------- | ------------------------------------------ | +| UID | `path` | string | `string` | | ✓ | | UID is the contact point unique identifier | +| Body | `body` | [EmbeddedContactPoint](#embedded-contact-point) | `models.EmbeddedContactPoint` | | | | | #### All responses @@ -704,10 +843,10 @@ PUT /api/v1/provisioning/mute-timings/{name} #### Parameters -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| ---- | ------ | --------------------------------------- | ------------------------- | --------- | :------: | ------- | ------------- | -| name | `path` | string | `string` | | ✓ | | Template Name | -| Body | `body` | [MuteTimeInterval](#mute-time-interval) | `models.MuteTimeInterval` | | | | | +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| ---- | ------ | --------------------------------------- | ------------------------- | --------- | :------: | ------- | ---------------- | +| name | `path` | string | `string` | | ✓ | | Mute timing name | +| Body | `body` | [MuteTimeInterval](#mute-time-interval) | `models.MuteTimeInterval` | | | | | #### All responses @@ -746,9 +885,9 @@ PUT /api/v1/provisioning/policies #### Parameters -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| ---- | ------ | --------------- | -------------- | --------- | :------: | ------- | ----------- | -| Body | `body` | [Route](#route) | `models.Route` | | | | | +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| ---- | ------ | --------------- | -------------- | --------- | :------: | ------- | ---------------------------------------- | +| Body | `body` | [Route](#route) | `models.Route` | | | | The new notification routing tree to use | #### All responses @@ -775,7 +914,7 @@ Status: Bad Request [ValidationError](#validation-error) -### Updates an existing template. (_RoutePutTemplate_) +### Updates an existing notification template. (_RoutePutTemplate_) ``` PUT /api/v1/provisioning/templates/{name} @@ -787,27 +926,27 @@ PUT /api/v1/provisioning/templates/{name} #### Parameters -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| ---- | ------ | -------------------------------------------------------- | ------------------------------------ | --------- | :------: | ------- | ------------- | -| name | `path` | string | `string` | | ✓ | | Template Name | -| Body | `body` | [NotificationTemplateContent](#message-template-content) | `models.NotificationTemplateContent` | | | | | +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| ---- | ------ | ------------------------------------------------------------- | ------------------------------------ | --------- | :------: | ------- | ------------- | +| name | `path` | string | `string` | | ✓ | | Template Name | +| Body | `body` | [NotificationTemplateContent](#notification-template-content) | `models.NotificationTemplateContent` | | | | | #### All responses -| Code | Status | Description | Has headers | Schema | -| ------------------------------ | ----------- | --------------- | :---------: | ---------------------------------------- | -| [202](#route-put-template-202) | Accepted | Ack | | [schema](#route-put-template-202-schema) | -| [400](#route-put-template-400) | Bad Request | ValidationError | | [schema](#route-put-template-400-schema) | +| Code | Status | Description | Has headers | Schema | +| ------------------------------ | ----------- | -------------------- | :---------: | ---------------------------------------- | +| [202](#route-put-template-202) | Accepted | NotificationTemplate | | [schema](#route-put-template-202-schema) | +| [400](#route-put-template-400) | Bad Request | ValidationError | | [schema](#route-put-template-400-schema) | #### Responses -##### 202 - Ack +##### 202 - NotificationTemplate Status: Accepted ###### Schema -[Ack](#ack) +[NotificationTemplate](#notification-template) ##### 400 - ValidationError @@ -817,57 +956,116 @@ Status: Bad Request [ValidationError](#validation-error) +### Clears the notification policy tree. (_RouteResetPolicyTree_) + +``` +DELETE /api/v1/provisioning/policies +``` + +#### Consumes + +- application/json + +#### All responses + +| Code | Status | Description | Has headers | Schema | +| ----------------------------------- | -------- | ----------- | :---------: | --------------------------------------------- | +| [202](#route-reset-policy-tree-202) | Accepted | Ack | | [schema](#route-reset-policy-tree-202-schema) | + +#### Responses + +##### 202 - Ack + +Status: Accepted + +###### Schema + +[Ack](#ack) + +## Models + +### Ack + +[interface{}](#interface) + ### AlertQuery **Properties** | Name | Type | Go type | Required | Default | Description | Example | | --------------------------------------------------------- | ----------------------------------------- | ------------------- | :------: | ------- | -------------------------------------------------------------------------------------------------- | ------- | -| DatasourceUID | string | `string` | | | Grafana data source unique identifier; it should be '-100' for a Server Side Expression operation. | | -| Model | object | `interface{}` | | | JSON is the raw JSON query and includes the above properties as well as custom properties. | | -| QueryType | string | `string` | | | QueryType is an optional identifier for the type of query. | +| datasourceUid | string | `string` | | | Grafana data source unique identifier; it should be '-100' for a Server Side Expression operation. | | +| model | [interface{}](#interface) | `interface{}` | | | JSON is the raw JSON query and includes the above properties as well as custom properties. | | +| queryType | string | `string` | | | QueryType is an optional identifier for the type of query. | | It can be used to distinguish different types of queries. | | -| RefID | string | `string` | | | RefID is the unique identifier of the query, set by the frontend call. | | +| refId | string | `string` | | | RefID is the unique identifier of the query, set by the frontend call. | | | relativeTimeRange | [RelativeTimeRange](#relative-time-range) | `RelativeTimeRange` | | | | | -### AlertRule +### AlertQueryExport **Properties** -| Name | Type | Go type | Required | Default | Description | Example | -| ------------ | ---------------------------- | ------------------- | :------: | ------- | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Annotations | map of string | `map[string]string` | | | | `{"runbook_url":"https://supercoolrunbook.com/page/13"}` | -| Condition | string | `string` | ✓ | | | `A` | -| Data | [][alertquery](#alert-query) | `[]*AlertQuery` | ✓ | | | `[{"datasourceUid":"-100","model":{"conditions":[{"evaluator":{"params":[0,0],"type":"gt"},"operator":{"type":"and"},"query":{"params":[]},"reducer":{"params":[],"type":"avg"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1 == 1","hide":false,"intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"},"queryType":"","refId":"A","relativeTimeRange":{"from":0,"to":0}}]` | -| ExecErrState | string | `string` | ✓ | | Allowed values: "OK", "Alerting", "Error" | | -| FolderUID | string | `string` | ✓ | | | `project_x` | -| ID | int64 (formatted integer) | `int64` | | | | | -| Labels | map of string | `map[string]string` | | | | `{"team":"sre-team-1"}` | -| NoDataState | string | `string` | ✓ | | Allowed values: "OK", "NoData", "Error" | | -| OrgID | int64 (formatted integer) | `int64` | ✓ | | | | -| RuleGroup | string | `string` | ✓ | | | `eval_group_1` | -| Title | string | `string` | ✓ | | | `Always firing` | -| UID | string | `string` | | | | | -| Updated | date-time (formatted string) | `strfmt.DateTime` | | | | | -| for | [Duration](#duration) | `Duration` | ✓ | | | | -| provenance | string | `Provenance` | | | | | +| Name | Type | Go type | Required | Default | Description | Example | +| ----------------- | ----------------------------------------- | ------------------- | :------: | ------- | ----------- | ------- | +| datasourceUid | string | `string` | | | | | +| model | [interface{}](#interface) | `interface{}` | | | | | +| queryType | string | `string` | | | | | +| refId | string | `string` | | | | | +| relativeTimeRange | [RelativeTimeRange](#relative-time-range) | `RelativeTimeRange` | | | | | + +### AlertRuleExport + +**Properties** + +| Name | Type | Go type | Required | Default | Description | Example | +| ------------ | ----------------------------------------- | --------------------- | :------: | ------- | ----------- | ------- | +| annotations | map of string | `map[string]string` | | | | | +| condition | string | `string` | | | | | +| dasboardUid | string | `string` | | | | | +| data | [][alertqueryexport](#alert-query-export) | `[]*AlertQueryExport` | | | | | +| execErrState | string | `string` | | | | | +| for | [Duration](#duration) | `Duration` | | | | | +| labels | map of string | `map[string]string` | | | | | +| noDataState | string | `string` | | | | | +| panelId | int64 (formatted integer) | `int64` | | | | | +| title | string | `string` | | | | | +| uid | string | `string` | | | | | ### AlertRuleGroup **Properties** -| Name | Type | Go type | Required | Default | Description | Example | -| -------- | ------------------------- | ------- | :------: | ------- | ----------- | ------- | -| Interval | int64 (formatted integer) | `int64` | | | | | +| Name | Type | Go type | Required | Default | Description | Example | +| --------- | ------------------------------------------------- | ------------------------- | :------: | ------- | ----------- | ------- | +| folderUid | string | `string` | | | | | +| interval | int64 (formatted integer) | `int64` | | | | | +| rules | [][provisionedalertrule](#provisioned-alert-rule) | `[]*ProvisionedAlertRule` | | | | | +| title | string | `string` | | | | | -### DayOfMonthRange +### AlertRuleGroupExport **Properties** -| Name | Type | Go type | Required | Default | Description | Example | -| ----- | ------------------------- | ------- | :------: | ------- | ----------- | ------- | -| Begin | int64 (formatted integer) | `int64` | | | | | -| End | int64 (formatted integer) | `int64` | | | | | +| Name | Type | Go type | Required | Default | Description | Example | +| -------- | --------------------------------------- | -------------------- | :------: | ------- | ----------- | ------- | +| folder | string | `string` | | | | | +| interval | [Duration](#duration) | `Duration` | | | | | +| name | string | `string` | | | | | +| orgId | int64 (formatted integer) | `int64` | | | | | +| rules | [][alertruleexport](#alert-rule-export) | `[]*AlertRuleExport` | | | | | + +### AlertingFileExport + +**Properties** + +| Name | Type | Go type | Required | Default | Description | Example | +| ---------- | -------------------------------------------------- | ------------------------- | :------: | ------- | ----------- | ------- | +| apiVersion | int64 (formatted integer) | `int64` | | | | | +| groups | [][alertrulegroupexport](#alert-rule-group-export) | `[]*AlertRuleGroupExport` | | | | | + +### ContactPoints + +[][embeddedcontactpoint](#embedded-contact-point) ### Duration @@ -877,25 +1075,35 @@ Status: Bad Request ### EmbeddedContactPoint -> EmbeddedContactPoint is the contact point integration that is used +> EmbeddedContactPoint is the contact point type that is used > by grafanas embedded alertmanager implementation. **Properties** -| Name | Type | Go type | Required | Default | Description | Example | -| --------------------- | ------- | -------- | :------: | ------- | ---------------------------------------------------------------------------------------------------- | ----------------------- | -| DisableResolveMessage | boolean | `bool` | | | | `false` | -| Name | string | `string` | ✓ | | Name is used as grouping key in the UI. Contact points with the same name will be grouped in the UI. | `webhook_1` | -| Provenance | string | `string` | | | | | -| Type | string | `string` | ✓ | | | `webhook` | -| UID | string | `string` | | | UID is the unique identifier of the contact point. The UID can be set by the user. | `my_external_reference` | -| settings | object | `JSON` | ✓ | | | | +| Name | Type | Go type | Required | Default | Description | Example | +| ------------------------------------ | ----------------------- | -------- | :------: | ------- | ----------------------------------------------------------------- | --------- | +| disableResolveMessage | boolean | `bool` | | | | `false` | +| name | string | `string` | | | Name is used as grouping key in the UI. Contact points with the | +| same name will be grouped in the UI. | `webhook_1` | +| provenance | string | `string` | | | | | +| settings | [JSON](#json) | `JSON` | ✓ | | | | +| type | string | `string` | ✓ | | | `webhook` | +| uid | string | `string` | | | UID is the unique identifier of the contact point. The UID can be | +| set by the user. | `my_external_reference` | + +### Json + +[interface{}](#interface) + +### MatchRegexps + +[MatchRegexps](#match-regexps) ### MatchType -| Name | Type | Go type | Default | Description | Example | -| --------- | ------------------------- | ------- | ------- | ---------------------------------------------------------------------- | ------- | -| MatchType | int64 (formatted integer) | int64 | | 0 = MatchEqual, 1 = MatchNotEqual, 2 = MatchRegexp, 3 = MatchNotRegexp | | +| Name | Type | Go type | Default | Description | Example | +| --------- | ------------------------- | ------- | ------- | ----------- | ------- | +| MatchType | int64 (formatted integer) | int64 | | | | ### Matcher @@ -915,49 +1123,40 @@ Status: Bad Request [][matcher](#matcher) -### NotificationTemplate - -**Properties** - -| Name | Type | Go type | Required | Default | Description | Example | -| ---------- | ------ | ------------ | :------: | ------- | ----------- | ------- | -| Name | string | `string` | | | | | -| Template | string | `string` | | | | | -| provenance | string | `Provenance` | | | | | - -### NotificationTemplateContent - -**Properties** - -| Name | Type | Go type | Required | Default | Description | Example | -| -------- | ------ | -------- | :------: | ------- | ----------- | ------- | -| Template | string | `string` | | | | | - -### MonthRange - -**Properties** - -| Name | Type | Go type | Required | Default | Description | Example | -| ----- | ------------------------- | ------- | :------: | ------- | ----------- | ------- | -| Begin | int64 (formatted integer) | `int64` | | | | | -| End | int64 (formatted integer) | `int64` | | | | | - ### MuteTimeInterval **Properties** -| Name | Type | Go type | Required | Default | Description | Example | -| ------------- | -------------------------------- | ----------------- | :------: | ------- | ----------- | ------- | -| Name | string | `string` | | | | | -| TimeIntervals | [][timeinterval](#time-interval) | `[]*TimeInterval` | | | | | +| Name | Type | Go type | Required | Default | Description | Example | +| -------------- | -------------------------------- | ----------------- | :------: | ------- | ----------- | ------- | +| name | string | `string` | | | | | +| time_intervals | [][timeinterval](#time-interval) | `[]*TimeInterval` | | | | | ### MuteTimings [][mutetimeinterval](#mute-time-interval) -### NotFound +### NotificationTemplate -[interface{}](#interface) +**Properties** + +| Name | Type | Go type | Required | Default | Description | Example | +| ---------- | ------------------------- | ------------ | :------: | ------- | ----------- | ------- | +| name | string | `string` | | | | | +| provenance | [Provenance](#provenance) | `Provenance` | | | | | +| template | string | `string` | | | | | + +### NotificationTemplateContent + +**Properties** + +| Name | Type | Go type | Required | Default | Description | Example | +| -------- | ------ | -------- | :------: | ------- | ----------- | ------- | +| template | string | `string` | | | | | + +### NotificationTemplates + +[][notificationtemplate](#notification-template) ### ObjectMatchers @@ -965,6 +1164,45 @@ Status: Bad Request #### Inlined models +### Provenance + +| Name | Type | Go type | Default | Description | Example | +| ---------- | ------ | ------- | ------- | ----------- | ------- | +| Provenance | string | string | | | | + +### ProvisionedAlertRule + +**Properties** + +| Name | Type | Go type | Required | Default | Description | Example | +| ------------ | ---------------------------- | ------------------- | :------: | ------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| annotations | map of string | `map[string]string` | | | | `{"runbook_url":"https://supercoolrunbook.com/page/13"}` | +| condition | string | `string` | ✓ | | | `A` | +| data | [][alertquery](#alert-query) | `[]*AlertQuery` | ✓ | | | `[{"datasourceUid":"-100","model":{"conditions":[{"evaluator":{"params":[0,0],"type":"gt"},"operator":{"type":"and"},"query":{"params":[]},"reducer":{"params":[],"type":"avg"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1 == 1","hide":false,"intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"},"queryType":"","refId":"A","relativeTimeRange":{"from":0,"to":0}}]` | +| execErrState | string | `string` | ✓ | | | | +| folderUID | string | `string` | ✓ | | | `project_x` | +| for | [Duration](#duration) | `Duration` | ✓ | | | | +| id | int64 (formatted integer) | `int64` | | | | | +| labels | map of string | `map[string]string` | | | | `{"team":"sre-team-1"}` | +| noDataState | string | `string` | ✓ | | | | +| orgID | int64 (formatted integer) | `int64` | ✓ | | | | +| provenance | [Provenance](#provenance) | `Provenance` | | | | | +| ruleGroup | string | `string` | ✓ | | | `eval_group_1` | +| title | string | `string` | ✓ | | | `Always firing` | +| uid | string | `string` | | | | | +| updated | date-time (formatted string) | `strfmt.DateTime` | | | | | + +### ProvisionedAlertRules + +[][provisionedalertrule](#provisioned-alert-rule) + +### Regexp + +> A Regexp is safe for concurrent use by multiple goroutines, +> except for configuration methods, such as Longest. + +[interface{}](#interface) + ### RelativeTimeRange > RelativeTimeRange is the per query start and end time @@ -979,22 +1217,26 @@ Status: Bad Request ### Route -> A Route is a node that contains definitions of how to handle alerts. +> A Route is a node that contains definitions of how to handle alerts. This is modified +> from the upstream alertmanager in that it adds the ObjectMatchers property. **Properties** -| Name | Type | Go type | Required | Default | Description | Example | -| ----------------- | ---------------------------------- | ---------------- | :------: | ------- | ----------- | ------- | -| Continue | boolean | `bool` | | | | | -| GroupByStr | []string | `[]string` | | | | | -| MuteTimeIntervals | []string | `[]string` | | | | | -| Receiver | string | `string` | | | | | -| Routes | [][route](#route) | `[]*Route` | | | | | -| group_interval | [Duration](#duration) | `Duration` | | | | | -| group_wait | [Duration](#duration) | `Duration` | | | | | -| object_matchers | [ObjectMatchers](#object-matchers) | `ObjectMatchers` | | | | | -| provenance | string | `Provenance` | | | | | -| repeat_interval | [Duration](#duration) | `Duration` | | | | | +| Name | Type | Go type | Required | Default | Description | Example | +| ------------------- | ---------------------------------- | ------------------- | :------: | ------- | --------------------------------------- | ------- | +| continue | boolean | `bool` | | | | | +| group_by | []string | `[]string` | | | | | +| group_interval | string | `string` | | | | | +| group_wait | string | `string` | | | | | +| match | map of string | `map[string]string` | | | Deprecated. Remove before v1.0 release. | | +| match_re | [MatchRegexps](#match-regexps) | `MatchRegexps` | | | | | +| matchers | [Matchers](#matchers) | `Matchers` | | | | | +| mute_time_intervals | []string | `[]string` | | | | | +| object_matchers | [ObjectMatchers](#object-matchers) | `ObjectMatchers` | | | | | +| provenance | [Provenance](#provenance) | `Provenance` | | | | | +| receiver | string | `string` | | | | | +| repeat_interval | string | `string` | | | | | +| routes | [][route](#route) | `[]*Route` | | | | | ### TimeInterval @@ -1003,13 +1245,14 @@ Status: Bad Request **Properties** -| Name | Type | Go type | Required | Default | Description | Example | -| ----------- | ---------------------------------------- | -------------------- | :------: | ------- | ----------- | ------- | -| DaysOfMonth | [][dayofmonthrange](#day-of-month-range) | `[]*DayOfMonthRange` | | | | | -| Months | [][monthrange](#month-range) | `[]*MonthRange` | | | | | -| Times | [][timerange](#time-range) | `[]*TimeRange` | | | | | -| Weekdays | [][weekdayrange](#weekday-range) | `[]*WeekdayRange` | | | | | -| Years | [][yearrange](#year-range) | `[]*YearRange` | | | | | +| Name | Type | Go type | Required | Default | Description | Example | +| ------------- | -------------------------- | -------------- | :------: | ------- | ----------- | ------- | +| days_of_month | []string | `[]string` | | | | | +| location | string | `string` | | | | | +| months | []string | `[]string` | | | | | +| times | [][timerange](#time-range) | `[]*TimeRange` | | | | | +| weekdays | []string | `[]string` | | | | | +| years | []string | `[]string` | | | | | ### TimeRange @@ -1026,24 +1269,6 @@ Status: Bad Request **Properties** -| Name | Type | Go type | Required | Default | Description | Example | -| ---- | ------ | -------- | :------: | ------- | ----------- | ------- | -| Msg | string | `string` | | | | | - -### WeekdayRange - -**Properties** - -| Name | Type | Go type | Required | Default | Description | Example | -| ----- | ------------------------- | ------- | :------: | ------- | ----------- | ------- | -| Begin | int64 (formatted integer) | `int64` | | | | | -| End | int64 (formatted integer) | `int64` | | | | | - -### YearRange - -**Properties** - -| Name | Type | Go type | Required | Default | Description | Example | -| ----- | ------------------------- | ------- | :------: | ------- | ----------- | ------- | -| Begin | int64 (formatted integer) | `int64` | | | | | -| End | int64 (formatted integer) | `int64` | | | | | +| Name | Type | Go type | Required | Default | Description | Example | +| ---- | ------ | -------- | :------: | ------- | ----------- | --------------- | +| msg | string | `string` | | | | `error message` | diff --git a/docs/sources/developers/kinds/composable/statushistorypanelcfg/schema-reference.md b/docs/sources/developers/kinds/composable/statushistorypanelcfg/schema-reference.md index 2daba7d77d2..46a8a21a125 100644 --- a/docs/sources/developers/kinds/composable/statushistorypanelcfg/schema-reference.md +++ b/docs/sources/developers/kinds/composable/statushistorypanelcfg/schema-reference.md @@ -34,6 +34,7 @@ title: StatusHistoryPanelCfg kind | Property | Type | Required | Description | |-------------|--------|----------|-----------------------------------------------------------| | `colWidth` | number | No | Controls the column width Default: `0.9`. | +| `rowHeight` | number | No | Set the height of the rows Default: `0.9`. | | `showValue` | string | No | TODO docs Possible values are: `auto`, `never`, `always`. | diff --git a/docs/sources/developers/kinds/core/dashboard/schema-reference.md b/docs/sources/developers/kinds/core/dashboard/schema-reference.md index 5d91eef9451..3672213e9d0 100644 --- a/docs/sources/developers/kinds/core/dashboard/schema-reference.md +++ b/docs/sources/developers/kinds/core/dashboard/schema-reference.md @@ -34,7 +34,7 @@ title: Dashboard kind | `templating` | [object](#templating) | No | TODO docs | | `time` | [object](#time) | No | Time range for dashboard, e.g. last 6 hours, last 7 days, etc | | `timepicker` | [object](#timepicker) | No | TODO docs
TODO this appears to be spread all over in the frontend. Concepts will likely need tidying in tandem with schema changes | -| `timezone` | string | No | Timezone of dashboard, Possible values are: `browser`, `utc`, ``. Default: `browser`. | +| `timezone` | string | No | Timezone of dashboard. Accepts IANA TZDB zone ID or "browser" or "utc". Default: `browser`. | | `title` | string | No | Title of dashboard. | | `uid` | string | No | Unique dashboard identifier that can be generated by anyone. string (8-40) | | `version` | integer | No | Version of the dashboard, incremented each time the dashboard is updated. | diff --git a/docs/sources/developers/kinds/core/librarypanel/schema-reference.md b/docs/sources/developers/kinds/core/librarypanel/schema-reference.md new file mode 100644 index 00000000000..6ac3ad434f7 --- /dev/null +++ b/docs/sources/developers/kinds/core/librarypanel/schema-reference.md @@ -0,0 +1,70 @@ +--- +keywords: + - grafana + - schema +title: LibraryPanel kind +--- +> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. + +# LibraryPanel kind + +## Maturity: experimental +## Version: 0.0 + +## Properties + +| Property | Type | Required | Description | +|-----------------|-------------------------------------------------|----------|--------------------------------------------------------------------------------------------------------------------------------------| +| `model` | [object](#model) | **Yes** | TODO: should be the same panel schema defined in dashboard
Typescript: Omit; | +| `name` | string | **Yes** | Panel name (also saved in the model) | +| `type` | string | **Yes** | The panel type (from inside the model) | +| `uid` | string | **Yes** | Library element UID | +| `version` | integer | **Yes** | panel version, incremented each time the dashboard is updated. | +| `description` | string | No | Panel description | +| `folderUid` | string | No | Folder UID | +| `meta` | [LibraryElementDTOMeta](#libraryelementdtometa) | No | | +| `schemaVersion` | integer | No | Dashboard version when this was saved (zero if unknown) | + +## LibraryElementDTOMeta + +### Properties + +| Property | Type | Required | Description | +|-----------------------|---------------------------------------------------------|----------|-------------| +| `connectedDashboards` | integer | **Yes** | | +| `createdBy` | [LibraryElementDTOMetaUser](#libraryelementdtometauser) | **Yes** | | +| `created` | string | **Yes** | | +| `folderName` | string | **Yes** | | +| `folderUid` | string | **Yes** | | +| `updatedBy` | [LibraryElementDTOMetaUser](#libraryelementdtometauser) | **Yes** | | +| `updated` | string | **Yes** | | + +### LibraryElementDTOMetaUser + +#### Properties + +| Property | Type | Required | Description | +|-------------|---------|----------|-------------| +| `avatarUrl` | string | **Yes** | | +| `id` | integer | **Yes** | | +| `name` | string | **Yes** | | + +### LibraryElementDTOMetaUser + +#### Properties + +| Property | Type | Required | Description | +|-------------|---------|----------|-------------| +| `avatarUrl` | string | **Yes** | | +| `id` | integer | **Yes** | | +| `name` | string | **Yes** | | + +## model + +TODO: should be the same panel schema defined in dashboard +Typescript: Omit; + +| Property | Type | Required | Description | +|----------|------|----------|-------------| + + diff --git a/docs/sources/developers/plugins/_index.md b/docs/sources/developers/plugins/_index.md index b166f602785..4502f42c250 100644 --- a/docs/sources/developers/plugins/_index.md +++ b/docs/sources/developers/plugins/_index.md @@ -24,8 +24,8 @@ Follow the questions and you will have a starter plugin ready to develop. If you want a more guided introduction to plugin development, check out our tutorials: -- [Build a panel plugin]({{< ref "build-a-panel-plugin.md" >}}) -- [Build a data source plugin]({{< ref "build-a-data-source-plugin.md" >}}) +- [Build a panel plugin](/tutorials/build-a-panel-plugin/) +- [Build a data source plugin](/tutorials/build-a-data-source-plugin/) ## Go further @@ -35,47 +35,47 @@ Learn more about specific areas of plugin development. If you're looking to build your first plugin, check out these introductory tutorials: -- [Build a panel plugin]({{< ref "build-a-panel-plugin.md" >}}) -- [Build a data source plugin]({{< ref "build-a-data-source-plugin.md" >}}) -- [Build a data source backend plugin]({{< ref "build-a-data-source-backend-plugin.md" >}}) +- [Build a panel plugin](/tutorials/build-a-panel-plugin/) +- [Build a data source plugin](/tutorials/build-a-data-source-plugin/) +- [Build a data source backend plugin](/tutorials/build-a-data-source-backend-plugin/) Ready to learn more? Check out our other tutorials: -- [Build a panel plugin with D3.js]({{< ref "build-a-panel-plugin-with-d3.md" >}}) +- [Build a panel plugin with D3.js](/tutorials/build-a-panel-plugin-with-d3/) ### Guides Improve an existing plugin with one of our guides: -- [Add authentication for data source plugins]({{< relref "add-authentication-for-data-source-plugins/" >}}) -- [Add support for annotations]({{< relref "add-support-for-annotations/" >}}) -- [Add support for Explore queries]({{< relref "add-support-for-explore-queries/" >}}) -- [Add support for variables]({{< relref "add-support-for-variables/" >}}) -- [Add a query editor help component]({{< relref "add-query-editor-help/" >}}) -- [Build a logs data source plugin]({{< relref "build-a-logs-data-source-plugin/" >}}) -- [Build a streaming data source plugin]({{< relref "build-a-streaming-data-source-plugin/" >}}) -- [Error handling]({{< relref "error-handling/" >}}) -- [Working with data frames]({{< relref "working-with-data-frames/" >}}) -- [Development with local Grafana]({{< relref "development-with-local-grafana/" >}}) +- [Add authentication for data source plugins](/tutorials/add-authentication-for-data-source-plugins/) +- [Add support for annotations](/tutorials/add-support-for-annotations/) +- [Add support for Explore queries](/tutorials/add-support-for-explore-queries/) +- [Add support for variables](/tutorials/add-support-for-variables/) +- [Add a query editor help component](/tutorials/add-query-editor-help/) +- [Build a logs data source plugin](/tutorials/build-a-logs-data-source-plugin/) +- [Build a streaming data source plugin](/tutorials/build-a-streaming-data-source-plugin/) +- [Error handling](/tutorials/error-handling/) +- [Working with data frames](/tutorials/working-with-data-frames/) +- [Development with local Grafana](/tutorials/development-with-local-grafana/) ### Concepts Deepen your knowledge through a series of high-level overviews of plugin concepts: -- [Data frames]({{< relref "data-frames/" >}}) +- [Data frames](/tutorials/data-frames/) ### UI library -Explore the many UI components in our [Grafana UI library](https://developers.grafana.com/ui). +Explore the many UI components in our [Grafana UI library](/ui). ### Examples -For inspiration, check out our [plugin examples](https://github.com/grafana/grafana-plugin-examples). +For inspiration, check out our [plugin examples](/grafana/grafana-plugin-examples). ### Metadata -- [Plugin metadata]({{< relref "metadata/" >}}) +- [Plugin metadata](/tutorials/metadata/) ### SDK -- [Grafana Plugin SDK for Go]({{< relref "backend/grafana-plugin-sdk-for-go/" >}}) +- [Grafana Plugin SDK for Go](/tutorials/backend/grafana-plugin-sdk-for-go/) diff --git a/docs/sources/developers/plugins/add-support-for-annotations.md b/docs/sources/developers/plugins/add-support-for-annotations.md index 154bd3a3d5c..f0ed19101e3 100644 --- a/docs/sources/developers/plugins/add-support-for-annotations.md +++ b/docs/sources/developers/plugins/add-support-for-annotations.md @@ -6,7 +6,7 @@ title: Add support for annotations This guide explains how to add support for [annotations]({{< relref "../../dashboards/build-dashboards/annotate-visualizations" >}}) to an existing data source plugin. -This guide assumes that you're already familiar with how to [Build a data source plugin]({{< ref "build-a-data-source-plugin.md" >}}). +This guide assumes that you're already familiar with how to [Build a data source plugin](/tutorials/build-a-data-source-plugin/). > **Note:** Annotation support for React plugins was released in Grafana 7.2. To support earlier versions, refer to [Add support for annotation for Grafana 7.1](https://grafana.com/docs/grafana/v7.1/developers/plugins/add-support-for-annotations/). diff --git a/docs/sources/developers/plugins/add-support-for-explore-queries.md b/docs/sources/developers/plugins/add-support-for-explore-queries.md index d3d59ba54cd..e2cc6fe32e2 100644 --- a/docs/sources/developers/plugins/add-support-for-explore-queries.md +++ b/docs/sources/developers/plugins/add-support-for-explore-queries.md @@ -6,7 +6,7 @@ title: Add support for Explore queries This guide explains how to improve support for [Explore]({{< relref "../../explore/" >}}) in an existing data source plugin. -This guide assumes that you're already familiar with how to [Build a data source plugin]({{< ref "build-a-data-source-plugin.md" >}}). +This guide assumes that you're already familiar with how to [Build a data source plugin](/tutorials/build-a-data-source-plugin/). With Explore, users can make ad-hoc queries without the use of a dashboard. This is useful when users want to troubleshoot or to learn more about the data. diff --git a/docs/sources/developers/plugins/build-a-logs-data-source-plugin.md b/docs/sources/developers/plugins/build-a-logs-data-source-plugin.md index 5c779425ebf..2df39a0315f 100644 --- a/docs/sources/developers/plugins/build-a-logs-data-source-plugin.md +++ b/docs/sources/developers/plugins/build-a-logs-data-source-plugin.md @@ -6,7 +6,7 @@ title: Build a logs data source plugin This guide explains how to build a logs data source plugin. -Data sources in Grafana supports both metrics and log data. The steps to build a logs data source plugin are largely the same as for a metrics data source. This guide assumes that you're already familiar with how to [Build a data source plugin]({{< ref "build-a-data-source-plugin.md" >}}) for metrics. +Data sources in Grafana supports both metrics and log data. The steps to build a logs data source plugin are largely the same as for a metrics data source. This guide assumes that you're already familiar with how to [Build a data source plugin](/tutorials/build-a-data-source-plugin/) for metrics. ## Add logs support to your data source diff --git a/docs/sources/developers/plugins/build-a-streaming-data-source-plugin.md b/docs/sources/developers/plugins/build-a-streaming-data-source-plugin.md index c49f936a5d5..feae0039a72 100644 --- a/docs/sources/developers/plugins/build-a-streaming-data-source-plugin.md +++ b/docs/sources/developers/plugins/build-a-streaming-data-source-plugin.md @@ -6,7 +6,7 @@ title: Build a streaming data source plugin This guide explains how to build a streaming data source plugin. -This guide assumes that you're already familiar with how to [Build a data source plugin]({{< ref "build-a-data-source-plugin.md" >}}). +This guide assumes that you're already familiar with how to [Build a data source plugin](/tutorials/build-a-data-source-plugin/). When monitoring critical applications, you want your dashboard to refresh as soon as your data does. In Grafana, you can set your dashboards to automatically refresh at a certain interval, no matter what data source you use. Unfortunately, this means that your queries are requesting all the data to be sent again, regardless of whether the data has actually changed. diff --git a/docs/sources/developers/plugins/sign-a-plugin.md b/docs/sources/developers/plugins/sign-a-plugin.md index ed626a7cdf9..6f611214160 100644 --- a/docs/sources/developers/plugins/sign-a-plugin.md +++ b/docs/sources/developers/plugins/sign-a-plugin.md @@ -32,7 +32,7 @@ To verify ownership of your plugin, you need to generate an API key that you'll Public plugins need to be reviewed by the Grafana team before you can sign them. -1. Submit your plugin for [review]({{< relref "package-a-plugin/#publishing-your-plugin-for-the-first-time" >}}) +1. Submit your plugin for [review]({{< relref "publish-a-plugin/#publishing-your-plugin-for-the-first-time" >}}) 2. When your plugin is approved, you're granted a plugin signature level. **Without a plugin signature level, you won't be able to sign your plugin**. 3. In your plugin directory, sign the plugin with the API key you just created. Grafana Sign Plugin creates a [MANIFEST.txt](#plugin-manifest) file in the `dist` directory of your plugin. diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index 8e3415c9af5..bc213e0bd06 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -772,7 +772,9 @@ Text used as placeholder text on login page for password input. ### default_theme -Set the default UI theme: `dark` or `light`. Default is `dark`. +Sets the default UI theme: `dark`, `light`, or `system`. The default theme is `dark`. + +`system` matches the user's system theme. ### default_language @@ -938,6 +940,21 @@ The following table shows the OAuth provider's setting with the default value an | GitLab | false | true | User organization roles are set with `defaultRole`, and the organization role can be changed for GitLab synced users. | | GitLab | true | true | User organization roles are set with `defaultRole` for GitLab. For other providers, the synchronization is skipped, and the org role can be changed, along with other OAuth provider users' org roles. | +### [auth.okta] skip_org_role_sync + +When a user logs in the first time, Grafana sets the organization role based on the value specified in `AutoAssignOrgRole`. If you want to manage organization roles through Grafana's UI, set the `skip_org_role_sync` option to `true`. +This also impacts `allow_assign_grafana_admin` setting, by not syncing the grafana admin role from GitLab. + +> **Note:** There is a separate setting called `oauth_skip_org_role_update_sync` which has a different scope. While `skip_org_role_sync` only applies to the specific OAuth provider, `oauth_skip_org_role_update_sync` is a generic setting that affects all configured OAuth providers. + +The following table shows the OAuth provider's setting with the default value and the skip org role sync setting. +| OAuth Provider | `oauth_skip_org_role_sync_update` | `skip_org_role_sync` | Behavior | +| --- | --- | --- | --- | +| Okta | false | false | User organization roles are set with `defaultRole` and cannot be changed. | +| Github | true | false | User organization roles are set with `defaultRole` for Okta, and Grafana Admins are set. For other providers, the synchronization is skipped, and the org role can be changed, along with other OAuth provider users' org roles. | +| Okta | false | true | User organization roles are set with `defaultRole`, and the organization role can be changed for Okta synced users. | +| Okta | true | true | User organization roles are set with `defaultRole` for Okta. For other providers, the synchronization is skipped, and the org role can be changed, along with other OAuth provider users' org roles. | + ### api_key_max_seconds_to_live Limit of API key seconds to live before expiration. Default is -1 (unlimited). 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 783c9be869f..449086347e2 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -28,8 +28,10 @@ Some stable features are enabled by default. You can disable a stable feature by | `commandPalette` | Enable command palette | Yes | | `cloudWatchDynamicLabels` | Use dynamic labels instead of alias patterns in CloudWatch datasource | Yes | | `internationalization` | Enables internationalization | Yes | +| `cloudWatchCrossAccountQuerying` | Enables cross-account querying in CloudWatch datasources | Yes | | `accessTokenExpirationCheck` | Enable OAuth access_token expiration check and token refresh using the refresh_token | | | `disablePrometheusExemplarSampling` | Disable Prometheus examplar sampling | | +| `logsSampleInExplore` | Enables access to the logs sample feature in Explore | Yes | ## Beta feature toggles @@ -83,7 +85,6 @@ Alpha features might be changed or removed without prior notice. | `dataConnectionsConsole` | Enables a new top-level page called Connections. This page is an experiment that provides a better experience when you install and configure data sources and other plugins. | | `topnav` | New top nav and page layouts | | `flameGraph` | Show the flame graph | -| `cloudWatchCrossAccountQuerying` | Use cross-account querying in CloudWatch datasource | | `redshiftAsyncQueryDataSupport` | Enable async query data support for Redshift | | `athenaAsyncQueryDataSupport` | Enable async query data support for Athena | | `increaseInMemDatabaseQueryCache` | Enable more in memory caching for database queries | @@ -97,23 +98,22 @@ Alpha features might be changed or removed without prior notice. | `sessionRemoteCache` | Enable using remote cache for user sessions | | `alertingBacktesting` | Rule backtesting API for alerting | | `editPanelCSVDragAndDrop` | Enables drag and drop for CSV and Excel files | -| `azureMultipleResourcePicker` | Azure multiple resource picker | +| `logsContextDatasourceUi` | Allow datasource to provide custom UI for context view | ## Development feature toggles The following toggles require explicitly setting Grafana's [app mode]({{< relref "../_index.md/#app_mode" >}}) to 'development' before you can enable this feature toggle. These features tend to be experimental. -| Feature toggle name | Description | -| -------------------------------------- | ----------------------------------------------------------------------- | -| `dashboardPreviewsAdmin` | Manage the dashboard previews crawler process from the UI | -| `showFeatureFlagsInUI` | Show feature flags in the settings UI | -| `publicDashboardsEmailSharing` | Allows public dashboard sharing to be restricted to only allowed emails | -| `k8s` | Explore native k8s integrations | -| `k8sDashboards` | Save dashboards via k8s | -| `dashboardsFromStorage` | Load dashboards from the generic storage interface | -| `export` | Export grafana instance (to git, etc) | -| `azureMonitorResourcePickerForMetrics` | New UI for Azure Monitor Metrics Query | -| `grpcServer` | Run GRPC server | -| `entityStore` | SQL-based entity store (requires storage flag also) | -| `queryLibrary` | Reusable query library | -| `nestedFolders` | Enable folder nesting | +| Feature toggle name | Description | +| ------------------------------ | ----------------------------------------------------------------------- | +| `dashboardPreviewsAdmin` | Manage the dashboard previews crawler process from the UI | +| `showFeatureFlagsInUI` | Show feature flags in the settings UI | +| `publicDashboardsEmailSharing` | Allows public dashboard sharing to be restricted to only allowed emails | +| `k8s` | Explore native k8s integrations | +| `k8sDashboards` | Save dashboards via k8s | +| `dashboardsFromStorage` | Load dashboards from the generic storage interface | +| `export` | Export grafana instance (to git, etc) | +| `grpcServer` | Run GRPC server | +| `entityStore` | SQL-based entity store (requires storage flag also) | +| `queryLibrary` | Reusable query library | +| `nestedFolders` | Enable folder nesting | diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/okta/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/okta/index.md index 4b3834cf72e..143d7eb0861 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/okta/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/okta/index.md @@ -130,6 +130,17 @@ Example: role_attribute_path = contains(groups[*], 'admin') && 'GrafanaAdmin' || contains(groups[*], 'editor') && 'Editor' || 'Viewer' ``` +## Skip organization role sync + +To prevent the sync of org roles from Okta, set `skip_org_role_sync` to `true`. This is useful if you want to manage the organization roles for your users from within Grafana. + +```ini +[auth.okta] +# .. +# prevents the sync of org roles from Okta +skip_org_role_sync = true +``` + ### Team Sync (Enterprise only) Map your Okta groups to teams in Grafana so that your users will automatically be added to diff --git a/docs/sources/shared/tutorials/create-plugin.md b/docs/sources/shared/tutorials/create-plugin.md new file mode 100755 index 00000000000..656bce3cf9e --- /dev/null +++ b/docs/sources/shared/tutorials/create-plugin.md @@ -0,0 +1,40 @@ +--- +title: Create Plugin +--- + +Tooling for modern web development can be tricky to wrap your head around. While you certainly can write your own webpack configuration, for this guide, you'll be using grafana create-plugin tool + +Grafana [create-plugin tool](https://www.npmjs.com/package/@grafana/create-plugin) is a CLI application that simplifies Grafana plugin development, so that you can focus on code. The tool scaffolds a starter plugin and all the required configuration for you. + +1. In the plugin directory, create a plugin from template using create-plugin: + + ``` + npx @grafana/create-plugin + ``` + +1. Change directory to your newly created plugin: + + ``` + cd my-plugin + ``` + +1. Install the dependencies: + + ``` + yarn install + ``` + +1. Build the plugin: + + ``` + yarn dev + ``` + +1. Restart the Grafana server for Grafana to discover your plugin. +1. Open Grafana and go to **Configuration** -> **Plugins**. Make sure that your plugin is there. + +By default, Grafana logs whenever it discovers a plugin: + +``` +INFO[01-01|12:00:00] Registering plugin logger=plugins name=my-plugin +``` diff --git a/docs/sources/shared/tutorials/plugin-anatomy.md b/docs/sources/shared/tutorials/plugin-anatomy.md new file mode 100644 index 00000000000..4d6e2c4ead0 --- /dev/null +++ b/docs/sources/shared/tutorials/plugin-anatomy.md @@ -0,0 +1,29 @@ +--- +title: Plugin Anatomy +--- + +Plugins come in different shapes and sizes. Before we dive deeper, let's look at some of the properties that are shared by all of them. + +Every plugin you create will require at least two files: `plugin.json` and `module.ts`. + +### plugin.json + +When Grafana starts, it scans the plugin directory for any subdirectory that contains a `plugin.json` file. The `plugin.json` file contains information about your plugin, and tells Grafana about what capabilities and dependencies your plugin needs. + +While certain plugin types can have specific configuration options, let's look at the mandatory ones: + +- `type` tells Grafana what type of plugin to expect. Grafana supports three types of plugins: `panel`, `datasource`, and `app`. +- `name` is what users will see in the list of plugins. If you're creating a data source, this is typically the name of the database it connects to, such as Prometheus, PostgreSQL, or Stackdriver. +- `id` uniquely identifies your plugin, and should start with your Grafana username, to avoid clashing with other plugins. [Sign up for a Grafana account](/signup/) to claim your username. + +To see all the available configuration settings for the `plugin.json`, refer to the [plugin.json Schema](/docs/grafana/latest/plugins/developing/plugin.json/). + +### module.ts + +After discovering your plugin, Grafana loads the `module.ts` file, the entrypoint for your plugin. `module.ts` exposes the implementation of your plugin, which depends on the type of plugin you're building. + +Specifically, `module.ts` needs to expose an object that extends [GrafanaPlugin](https://github.com/grafana/grafana/blob/08bf2a54523526a7f59f7c6a8dafaace79ab87db/packages/grafana-data/src/types/plugin.ts#L124), and can be any of the following: + +- [PanelPlugin](https://github.com/grafana/grafana/blob/08bf2a54523526a7f59f7c6a8dafaace79ab87db/packages/grafana-data/src/types/panel.ts#L73) +- [DataSourcePlugin](https://github.com/grafana/grafana/blob/08bf2a54523526a7f59f7c6a8dafaace79ab87db/packages/grafana-data/src/types/datasource.ts#L33) +- [AppPlugin](https://github.com/grafana/grafana/blob/45b7de1910819ad0faa7a8aeac2481e675870ad9/packages/grafana-data/src/types/app.ts#L27) diff --git a/docs/sources/shared/tutorials/publish-your-plugin.md b/docs/sources/shared/tutorials/publish-your-plugin.md new file mode 100644 index 00000000000..097f2417608 --- /dev/null +++ b/docs/sources/shared/tutorials/publish-your-plugin.md @@ -0,0 +1,77 @@ +--- +title: Package your plugin +--- + +Once you're happy with your plugin, it's time to package it, and submit to the plugin repository. + +For users to be able to use the plugin without building it themselves, you need to make a production build of the plugin, and commit to a release branch in your repository. + +To submit a plugin to the plugin repository, you need to create a release of your plugin. While we recommend following the branching strategy outlined below, you're free to use one that makes more sense to you. + +#### Create a plugin release + +Let's create version 0.1.0 of our plugin. + +1. Create a branch called `release-0.1.x`. + + ``` + git checkout -b release-0.1.x + ``` + +1. Do a production build. + + ``` + yarn build + ``` + +1. Add the `dist` directory. + + ``` + git add -f dist + ``` + +1. Create the release commit. + + ``` + git commit -m "Release v0.1.0" + ``` + +1. Create a release tag. + + ``` + git tag -a v0.1.0 -m "Create release tag v0.1.0" + ``` + +1. Push to GitHub. `follow-tags` tells Git to push the release tag along with our release branch. + ``` + git push --set-upstream origin release-0.1.x --follow-tags + ``` + +#### Submit the plugin + +For a plugin to be published on [Grafana Plugins](/grafana/plugins/), it needs to be added to the [grafana-plugin-repository](https://github.com/grafana/grafana-plugin-repository). + +1. Fork the [grafana-plugin-repository](https://github.com/grafana/grafana-plugin-repository) + +1. Add your plugin to the `repo.json` file in the project root directory: + + ```json + { + "id": "", + "type": "", + "url": "https://github.com//my-plugin", + "versions": [ + { + "version": "", + "commit": "", + "url": "https://github.com//my-plugin" + } + ] + } + ``` + +1. [Create a pull request](https://github.com/grafana/grafana-plugin-repository/pull/new/master). + +Once your plugin has been accepted, it'll be published on [Grafana Plugin](/grafana/plugins/), available for anyone to [install](/docs/grafana/latest/plugins/installation/)! + +> We're auditing every plugin that's added to make sure it's ready to be published. This means that it might take some time before your plugin is accepted. We're working on adding more automated tests to improve this process. diff --git a/docs/sources/shared/tutorials/set-up-environment.md b/docs/sources/shared/tutorials/set-up-environment.md new file mode 100644 index 00000000000..07eb372727a --- /dev/null +++ b/docs/sources/shared/tutorials/set-up-environment.md @@ -0,0 +1,34 @@ +--- +title: Set up Environment +--- + +Before you can get started building plugins, you need to set up your environment for plugin development. + +To discover plugins, Grafana scans a _plugin directory_, the location of which depends on your operating system. + +1. Create a directory called `grafana-plugins` in your preferred workspace. + +1. Find the `plugins` property in the Grafana configuration file and set the `plugins` property to the path of your `grafana-plugins` directory. Refer to the [Grafana configuration documentation](/docs/grafana/latest/installation/configuration/#plugins) for more information. + + ```ini + [paths] + plugins = "/path/to/grafana-plugins" + ``` + +1. Restart Grafana if it's already running, to load the new configuration. + +### Alternative method: Docker + +If you don't want to install Grafana on your local machine, you can use [Docker](https://www.docker.com). + +To set up Grafana for plugin development using Docker, run the following command: + +``` +docker run -d -p 3000:3000 -v "$(pwd)"/grafana-plugins:/var/lib/grafana/plugins --name=grafana grafana/grafana:7.0.0 +``` + +Since Grafana only loads plugins on start-up, you need to restart the container whenever you add or remove a plugin. + +``` +docker restart grafana +``` diff --git a/docs/sources/tutorials/_index.md b/docs/sources/tutorials/_index.md new file mode 100644 index 00000000000..cfe6c7633df --- /dev/null +++ b/docs/sources/tutorials/_index.md @@ -0,0 +1,9 @@ +--- +title: 'Tutorials' +menuTitle: 'Tutorials' +description: 'Grafana tutorials' +--- + +# Tutorials + +{{< section >}} diff --git a/docs/sources/tutorials/build-a-data-source-backend-plugin/index.md b/docs/sources/tutorials/build-a-data-source-backend-plugin/index.md new file mode 100644 index 00000000000..ebc6c160926 --- /dev/null +++ b/docs/sources/tutorials/build-a-data-source-backend-plugin/index.md @@ -0,0 +1,180 @@ +--- +title: Build a data source backend plugin +summary: Create a backend for your data source plugin. +description: Create a backend for your data source plugin. +id: build-a-data-source-backend-plugin +categories: ['plugins'] +tags: ['beginner'] +status: Published +authors: ['grafana_labs'] +Feedback Link: https://github.com/grafana/tutorials/issues/new +weight: 75 +--- + +## Introduction + +Grafana supports a wide range of data sources, including Prometheus, MySQL, and even Datadog. There's a good chance you can already visualize metrics from the systems you have set up. In some cases, though, you already have an in-house metrics solution that you’d like to add to your Grafana dashboards. This tutorial teaches you to build a support for your data source. + +For more information about backend plugins, refer to the documentation on [Backend plugins](/docs/grafana/latest/developers/plugins/backend/). + +In this tutorial, you'll: + +- Build a backend for your data source +- Implement a health check for your data source +- Enable Grafana Alerting for your data source + +{{% class "prerequisite-section" %}} + +#### Prerequisites + +- Knowledge about how data sources are implemented in the frontend. +- Grafana 7.0 +- Go ([Version](https://github.com/grafana/plugin-tools/blob/main/packages/create-plugin/templates/backend/go.mod#L3)) +- [Mage](https://magefile.org/) +- NodeJS ([Version](https://github.com/grafana/plugin-tools/blob/main/packages/create-plugin/templates/common/package.json#L66)) +- yarn + {{% /class %}} + +## Set up your environment + +{{< docs/shared lookup="tutorials/set-up-environment.md" source="grafana" version="latest" >}} + +## Create a new plugin + +To build a backend for your data source plugin, Grafana requires a binary that it can execute when it loads the plugin during start-up. In this guide, we will build a binary using the [Grafana plugin SDK for Go](/docs/grafana/latest/developers/plugins/backend/grafana-plugin-sdk-for-go/). + +The easiest way to get started is to use the Grafana [create-plugin tool](https://www.npmjs.com/package/@grafana/create-plugin). Navigate to the plugin folder that you configured in step 1 and type: + +``` +npx @grafana/create-plugin +``` + +Follow the steps and select **datasource** as your plugin type and answer **yes** when prompted to create a backend for your plugin. + +```bash +cd my-plugin +``` + +Install frontend dependencies and build frontend parts of the plugin to _dist_ directory: + +```bash +yarn install +yarn build +``` + +Run the following to update [Grafana plugin SDK for Go](/docs/grafana/latest/developers/plugins/backend/grafana-plugin-sdk-for-go/) dependency to the latest minor version: + +```bash +go get -u github.com/grafana/grafana-plugin-sdk-go +go mod tidy +``` + +Build backend plugin binaries for Linux, Windows and Darwin to _dist_ directory: + +```bash +mage -v +``` + +Now, let's verify that the plugin you've built so far can be used in Grafana when creating a new data source: + +1. Restart your Grafana instance. +1. Open Grafana in your web browser. +1. Navigate via the side-menu to **Configuration** -> **Data Sources**. +1. Click **Add data source**. +1. Find your newly created plugin and select it. +1. Enter a name and then click **Save & Test** (ignore any errors reported for now). + +You now have a new data source instance of your plugin that is ready to use in a dashboard: + +1. Navigate via the side-menu to **Create** -> **Dashboard**. +1. Click **Add new panel**. +1. In the query tab, select the data source you just created. +1. A line graph is rendered with one series consisting of two data points. +1. Save the dashboard. + +### Troubleshooting + +#### Grafana doesn't load my plugin + +By default, Grafana requires backend plugins to be signed. To load unsigned backend plugins, you need to +configure Grafana to [allow unsigned plugins](/docs/grafana/latest/plugins/plugin-signature-verification/#allow-unsigned-plugins). +For more information, refer to [Plugin signature verification](/docs/grafana/latest/plugins/plugin-signature-verification/#backend-plugins). + +## Anatomy of a backend plugin + +The folders and files used to build the backend for the data source are: + +| file/folder | description | +| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Magefile.go` | It’s not a requirement to use mage build files, but we strongly recommend using it so that you can use the build targets provided by the plugin SDK. | +| `/go.mod ` | Go modules dependencies, [reference](https://golang.org/cmd/go/#hdr-The_go_mod_file) | +| `/src/plugin.json` | A JSON file describing the backend plugin | +| `/pkg/main.go` | Starting point of the plugin binary. | + +#### plugin.json + +The [plugin.json](/docs/grafana/latest/developers/plugins/metadata/) file is required for all plugins. When building a backend plugin these properties are important: + +| property | description | +| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| backend | Should be set to `true` for backend plugins. This tells Grafana that it should start a binary when loading the plugin. | +| executable | This is the name of the executable that Grafana expects to start, see [plugin.json reference](/docs/grafana/latest/developers/plugins/metadata/) for details. | +| alerting | Should be set to `true` if your backend datasource supports alerting. | + +In the next step we will look at the query endpoint! + +## Implement data queries + +We begin by opening the file `/pkg/plugin/plugin.go`. In this file you will see the `SampleDatasource` struct which implements the [backend.QueryDataHandler](https://pkg.go.dev/github.com/grafana/grafana-plugin-sdk-go/backend?tab=doc#QueryDataHandler) interface. The `QueryData` method on this struct is where the data fetching happens for a data source plugin. + +Each request contains multiple queries to reduce traffic between Grafana and plugins. So you need to loop over the slice of queries, process each query, and then return the results of all queries. + +In the tutorial we have extracted a method named `query` to take care of each query model. Since each plugin has their own unique query model, Grafana sends it to the backend plugin as JSON. Therefore the plugin needs to `Unmarshal` the query model into something easier to work with. + +As you can see the sample only returns static numbers. Try to extend the plugin to return other types of data. + +You can read more about how to [build data frames in our docs](/docs/grafana/latest/developers/plugins/data-frames/). + +## Add support for health checks + +Implementing the health check handler allows Grafana to verify that a data source has been configured correctly. + +When editing a data source in Grafana's UI, you can **Save & Test** to verify that it works as expected. + +In this sample data source, there is a 50% chance that the health check will be successful. Make sure to return appropriate error messages to the users, informing them about what is misconfigured in the data source. + +Open `/pkg/plugin/plugin.go`. In this file you'll see that the `SampleDatasource` struct also implements the [backend.CheckHealthHandler](https://pkg.go.dev/github.com/grafana/grafana-plugin-sdk-go/backend?tab=doc#CheckHealthHandler) interface. Navigate to the `CheckHealth` method to see how the health check for this sample plugin is implemented. + +## Enable Grafana Alerting + +1. Open _src/plugin.json_. +1. Add the top level `backend` property with a value of `true` to specify that your plugin supports Grafana Alerting, e.g. + ```json + { + ... + "backend": true, + "executable": "gpx_simple_datasource_backend", + "alerting": true, + "info": { + ... + } + ``` +1. Rebuild frontend parts of the plugin to _dist_ directory: + +```bash +yarn build +``` + +1. Restart your Grafana instance. +1. Open Grafana in your web browser. +1. Open the dashboard you created earlier in the _Create a new plugin_ step. +1. Edit the existing panel. +1. Click on the _Alert_ tab. +1. Click on _Create Alert_ button. +1. Edit condition and specify _IS ABOVE 10_. Change _Evaluate every_ to _10s_ and clear the _For_ field to make the alert rule evaluate quickly. +1. Save the dashboard. +1. After some time the alert rule evaluates and transitions into _Alerting_ state. + +## Summary + +In this tutorial you created a backend for your data source plugin. diff --git a/docs/sources/tutorials/build-a-data-source-plugin/index.md b/docs/sources/tutorials/build-a-data-source-plugin/index.md new file mode 100644 index 00000000000..8cb9af04281 --- /dev/null +++ b/docs/sources/tutorials/build-a-data-source-plugin/index.md @@ -0,0 +1,372 @@ +--- +title: Build a data source plugin +summary: Create a plugin to add support for your own data sources. +description: Create a plugin to add support for your own data sources. +id: build-a-data-source-plugin +categories: ['plugins'] +tags: ['beginner'] +status: Published +authors: ['grafana_labs'] +Feedback Link: https://github.com/grafana/tutorials/issues/new +weight: 70 +--- + +## Introduction + +Grafana supports a wide range of data sources, including Prometheus, MySQL, and even Datadog. There's a good chance you can already visualize metrics from the systems you have set up. In some cases, though, you already have an in-house metrics solution that you’d like to add to your Grafana dashboards. This tutorial teaches you to build a support for your data source. + +In this tutorial, you'll: + +- Build a data source to visualize a sine wave +- Construct queries using the query editor +- Configure your data source using the config editor + +{{% class "prerequisite-section" %}} + +### Prerequisites + +- Grafana >=7.0 +- NodeJS >=14 +- yarn + {{% /class %}} + +## Set up your environment + +{{< docs/shared lookup="tutorials/set-up-environment.md" source="grafana" version="latest" >}} + +## Create a new plugin + +{{< docs/shared lookup="tutorials/create-plugin.md" source="grafana" version="latest" >}} + +## Anatomy of a plugin + +{{< docs/shared lookup="tutorials/plugin-anatomy.md" source="grafana" version="latest" >}} + +## Data source plugins + +A data source in Grafana must extend the `DataSourceApi` interface, which requires you to defines two methods: `query` and `testDatasource`. + +### The `query` method + +The `query` method is the heart of any data source plugin. It accepts a query from the user, retrieves the data from an external database, and returns the data in a format that Grafana recognizes. + +``` +async query(options: DataQueryRequest): Promise +``` + +The `options` object contains the queries, or _targets_, that the user made, along with context information, like the current time interval. Use this information to query an external database. + +> The term _target_ originates from Graphite, and the earlier days of Grafana when Graphite was the only supported data source. As Grafana gained support for more data sources, the term "target" became synonymous with any type of query. + +### Test your data source + +`testDatasource` implements a health check for your data source. For example, Grafana calls this method whenever the user clicks the **Save & Test** button, after changing the connection settings. + +``` +async testDatasource() +``` + +## Data frames + +Nowadays there are countless of different databases, each with their own ways of querying data. To be able to support all the different data formats, Grafana consolidates the data into a unified data structure called _data frames_. + +Let's see how to create and return a data frame from the `query` method. In this step, you'll change the code in the starter plugin to return a [sine wave](https://en.wikipedia.org/wiki/Sine_wave). + +1. In the current `query` method, remove the code inside the `map` function. + + The `query` method now look like this: + + ```ts + async query(options: DataQueryRequest): Promise { + const { range } = options; + const from = range!.from.valueOf(); + const to = range!.to.valueOf(); + + const data = options.targets.map(target => { + // Your code goes here. + }); + + return { data }; + } + ``` + +1. In the `map` function, use the `lodash/defaults` package to set default values for query properties that haven't been set: + + ```ts + const query = defaults(target, defaultQuery); + ``` + +1. Create a data frame with a time field and a number field: + + ```ts + const frame = new MutableDataFrame({ + refId: query.refId, + fields: [ + { name: 'time', type: FieldType.time }, + { name: 'value', type: FieldType.number }, + ], + }); + ``` + + `refId` needs to be set to tell Grafana which query that generated this date frame. + +Next, we'll add the actual values to the data frame. Don't worry about the math used to calculate the values. + +1. Create a couple of helper variables: + + ```ts + // duration of the time range, in milliseconds. + const duration = to - from; + + // step determines how close in time (ms) the points will be to each other. + const step = duration / 1000; + ``` + +1. Add the values to the data frame: + + ```ts + for (let t = 0; t < duration; t += step) { + frame.add({ time: from + t, value: Math.sin((2 * Math.PI * t) / duration) }); + } + ``` + + The `frame.add()` accepts an object where the keys corresponds to the name of each field in the data frame. + +1. Return the data frame: + + ```ts + return frame; + ``` + +1. Rebuild the plugin and try it out. + +Your data source is now sending data frames that Grafana can visualize. Next, we'll look at how you can control the frequency of the sine wave by defining a _query_. + +> In this example, we're generating timestamps from the current time range. This means that you'll get the same graph no matter what time range you're using. In practice, you'd instead use the timestamps returned by your database. + +## Define a query + +Most data sources offer a way to query specific data. MySQL and PostgreSQL use SQL, while Prometheus has its own query language, called _PromQL_. No matter what query language your databases are using, Grafana lets you build support for it. + +Add support for custom queries to your data source, by implementing your own _query editor_, a React component that enables users to build their own queries, through a user-friendly graphical interface. + +A query editor can be as simple as a text field where the user edits the raw query text, or it can provide a more user-friendly form with drop-down menus and switches, that later gets converted into the raw query text before it gets sent off to the database. + +### Define the query model + +The first step in designing your query editor is to define its _query model_. The query model defines the user input to your data source. + +We want to be able to control the frequency of the sine wave, so let's add another property. + +1. Add a new number property called `frequency` to the query model: + + **src/types.ts** + + ```ts + export interface MyQuery extends DataQuery { + queryText?: string; + constant: number; + frequency: number; + } + ``` + +1. Set a default value to the new `frequency` property: + + ```ts + export const defaultQuery: Partial = { + constant: 6.5, + frequency: 1.0, + }; + ``` + +### Bind the model to a form + +Now that you've defined the query model you wish to support, the next step is to bind the model to a form. The `FormField` is a text field component from `grafana/ui` that lets you register a listener which will be invoked whenever the form field value changes. + +1. Add a new form field to the query editor to control the new frequency property. + + **QueryEditor.tsx** + + ```ts + const { queryText, constant, frequency } = query; + ``` + + ```ts + + ``` + +1. Add a event listener for the new property. + + ```ts + onFrequencyChange = (event: ChangeEvent) => { + const { onChange, query, onRunQuery } = this.props; + onChange({ ...query, frequency: parseFloat(event.target.value) }); + // executes the query + onRunQuery(); + }; + ``` + + The registered listener, `onFrequencyChange`, calls `onChange` to update the current query with the value from the form field. + + `onRunQuery();` tells Grafana to run the query after each change. For fast queries, this is recommended to provide a more responsive experience. + +### Use the property + +The new query model is now ready to use in our `query` method. + +1. In the `query` method, use the `frequency` property to adjust our equation. + + ```ts + frame.add({ time: from + t, value: Math.sin((2 * Math.PI * query.frequency * t) / duration) }); + ``` + +## Configure your data source + +To access a specific data source, you often need to configure things like hostname, credentials, or authentication method. A _config editor_ lets your users configure your data source plugin to fit their needs. + +The config editor looks similar to the query editor, in that it defines a model and binds it to a form. + +Since we're not actually connecting to an external database in our sine wave example, we don't really need many options. To show you how you can add an option however, we're going to add the _wave resolution_ as an option. + +The resolution controls how close in time the data points are to each other. A higher resolution means more points closer together, at the cost of more data being processed. + +### Define the options model + +1. Add a new number property called `resolution` to the options model. + + **types.ts** + + ```ts + export interface MyDataSourceOptions extends DataSourceJsonData { + path?: string; + resolution?: number; + } + ``` + +### Bind the model to a form + +Just like query editor, the form field in the config editor calls the registered listener whenever the value changes. + +1. Add a new form field to the query editor to control the new resolution option. + + **ConfigEditor.tsx** + + ```ts +
+ +
+ ``` + +1. Add a event listener for the new option. + + ```ts + onResolutionChange = (event: ChangeEvent) => { + const { onOptionsChange, options } = this.props; + const jsonData = { + ...options.jsonData, + resolution: parseFloat(event.target.value), + }; + onOptionsChange({ ...options, jsonData }); + }; + ``` + + The `onResolutionChange` listener calls `onOptionsChange` to update the current options with the value from the form field. + +### Use the option + +1. Create a property called `resolution` to the `DataSource` class. + + ```ts + export class DataSource extends DataSourceApi { + resolution: number; + + constructor(instanceSettings: DataSourceInstanceSettings) { + super(instanceSettings); + + this.resolution = instanceSettings.jsonData.resolution || 1000.0; + } + + // ... + ``` + +1. In the `query` method, use the `resolution` property to calculate the step size. + + **src/DataSource.ts** + + ```ts + const step = duration / this.resolution; + ``` + +## Get data from an external API + +So far, you've generated the data returned by the data source. A more realistic use case would be to fetch data from an external API. + +While you can use something like [axios](https://github.com/axios/axios) or the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) to make requests, we recommend using the [`getBackendSrv`](/docs/grafana/latest/packages_api/runtime/getbackendsrv/) function from the [grafana/runtime](/docs/grafana/latest/packages_api/runtime/) package. + +The main advantage of `getBackendSrv` is that it proxies requests through the Grafana server rather making the request from the browser. This is strongly recommended when making authenticated requests to an external API. For more information on authenticating external requests, refer to [Add authentication for data source plugins](/docs/grafana/latest/developers/plugins/add-authentication-for-data-source-plugins/). + +1. Import `getBackendSrv`. + + **src/DataSource.ts** + + ```ts + import { getBackendSrv } from '@grafana/runtime'; + ``` + +1. Create a helper method `doRequest` and use the `datasourceRequest` method to make a request to your API. Replace `https://api.example.com/metrics` to point to your own API endpoint. + + ```ts + async doRequest(query: MyQuery) { + const result = await getBackendSrv().datasourceRequest({ + method: "GET", + url: "https://api.example.com/metrics", + params: query, + }) + + return result; + } + ``` + +1. Make a request for each query. `Promises.all` waits for all requests to finish before returning the data. + + ```ts + async query(options: DataQueryRequest): Promise { + const promises = options.targets.map((query) => + this.doRequest(query).then((response) => { + const frame = new MutableDataFrame({ + refId: query.refId, + fields: [ + { name: "Time", type: FieldType.time }, + { name: "Value", type: FieldType.number }, + ], + }); + + response.data.forEach((point: any) => { + frame.appendRow([point.time, point.value]); + }); + + return frame; + }) + ); + + return Promise.all(promises).then((data) => ({ data })); + } + ``` + +## Summary + +In this tutorial you built a complete data source plugin for Grafana that uses a query editor to control what data to visualize. You've added a data source option, commonly used to set connection options and more. + +### Learn more + +Learn how you can improve your plugin even further, by reading our advanced guides: + +- [Add support for variables](/docs/grafana/latest/developers/plugins/add-support-for-variables/) +- [Add support for annotations](/docs/grafana/latest/developers/plugins/add-support-for-annotations/) +- [Add support for Explore queries](/docs/grafana/latest/developers/plugins/add-support-for-explore-queries/) +- [Build a logs data source](/docs/grafana/latest/developers/plugins/build-a-logs-data-source-plugin/) diff --git a/docs/sources/tutorials/build-a-panel-plugin-with-d3/index.md b/docs/sources/tutorials/build-a-panel-plugin-with-d3/index.md new file mode 100644 index 00000000000..3638df56e06 --- /dev/null +++ b/docs/sources/tutorials/build-a-panel-plugin-with-d3/index.md @@ -0,0 +1,235 @@ +--- +title: Build a panel plugin with D3.js +summary: Learn how to use D3.js in your panel plugins. +description: how to use D3.js in your panel plugins. +id: build-a-panel-plugin-with-d3 +categories: ['plugins'] +tags: ['beginner'] +status: Published +authors: ['grafana_labs'] +Feedback Link: https://github.com/grafana/tutorials/issues/new +weight: 60 +--- + +## Introduction + +Panels are the building blocks of Grafana, and allow you to visualize data in different ways. This tutorial gives you a hands-on walkthrough of creating your own panel using [D3.js](https://d3js.org/). + +For more information about panels, refer to the documentation on [Panels](/docs/grafana/latest/features/panels/panels/). + +In this tutorial, you'll: + +- Build a simple panel plugin to visualize a bar chart. +- Learn how to use D3.js to build a panel using data-driven transformations. + +{{% class "prerequisite-section" %}} + +### Prerequisites + +- Grafana 7.0 +- NodeJS 12.x +- yarn + {{% /class %}} + +## Set up your environment + +{{< docs/shared lookup="tutorials/set-up-environment.md" source="grafana" version="latest" >}} + +## Create a new plugin + +{{< docs/shared lookup="tutorials/create-plugin.md" source="grafana" version="latest" >}} + +## Data-driven documents + +[D3.js](https://d3js.org/) is a JavaScript library for manipulating documents based on data. It lets you transform arbitrary data into HTML, and is commonly used for creating visualizations. + +Wait a minute. Manipulating documents based on data? That's sounds an awful lot like React. In fact, much of what you can accomplish with D3 you can already do with React. So before we start looking at D3, let's see how you can create an SVG from data, using only React. + +In **SimplePanel.tsx**, change `SimplePanel` to return an `svg` with a `rect` element. + +```ts +export const SimplePanel: React.FC = ({ options, data, width, height }) => { + const theme = useTheme(); + + return ( + + + + ); +}; +``` + +One single rectangle might not be very exciting, so let's see how you can create rectangles from data. + +1. Create some data that we can visualize. + + ```ts + const values = [4, 8, 15, 16, 23, 42]; + ``` + +1. Calculate the height of each bar based on the height of the panel. + + ```ts + const barHeight = height / values.length; + ``` + +1. Inside a SVG group, `g`, create a `rect` element for every value in the dataset. Each rectangle uses the value as its width. + + ```ts + return ( + + + {values.map((value, i) => ( + + ))} + + + ); + ``` + +1. Rebuild the plugin and reload your browser to see the changes you've made. + +As you can see, React is perfectly capable of dynamically creating HTML elements. In fact, creating elements using React is often faster than creating them using D3. + +So why would you use even use D3? In the next step, we'll see how you can take advantage of D3's data transformations. + +## Transform data using D3.js + +In this step, you'll see how you can transform data using D3 before rendering it using React. + +D3 is already bundled with Grafana, and you can access it by importing the `d3` package. However, we're going to need the type definitions while developing. + +1. Install the D3 type definitions: + + ```bash + yarn add --dev @types/d3 + ``` + +1. Import `d3` in **SimplePanel.tsx**. + + ```ts + import * as d3 from 'd3'; + ``` + +In the previous step, we had to define the width of each bar in pixels. Instead, let's use _scales_ from the D3 library to make the width of each bar depend on the width of the panel. + +Scales are functions that map a range of values to another range of values. In this case, we want to map the values in our datasets to a position within our panel. + +1. Create a scale to map a value between 0 and the maximum value in the dataset, to a value between 0 and the width of the panel. We'll be using this to calculate the width of the bar. + + ```ts + const scale = d3 + .scaleLinear() + .domain([0, d3.max(values) || 0.0]) + .range([0, width]); + ``` + +1. Pass the value to the scale function to calculate the width of the bar in pixels. + + ```ts + return ( + + + {values.map((value, i) => ( + + ))} + + + ); + ``` + +As you can see, even if we're using React to render the actual elements, the D3 library contains useful tools that you can use to transform your data before rendering it. + +## Add an axis + +Another useful tool in the D3 toolbox is the ability to generate _axes_. Adding axes to our chart makes it easier for the user to understand the differences between each bar. + +Let's see how you can use D3 to add a horizontal axis to your bar chart. + +1. Create a D3 axis. Notice that by using the same scale as before, we make sure that the bar width aligns with the ticks on the axis. + + ```ts + const axis = d3.axisBottom(scale); + ``` + +1. Generate the axis. While D3 needs to generate the elements for the axis, we can encapsulate it by generating them within an anonymous function which we pass as a `ref` to a group element `g`. + + ```ts + { + d3.select(node).call(axis as any); + }} + /> + ``` + +By default, the axis renders at the top of the SVG element. We'd like to move it to the bottom, but to do that, we first need to make room for it by decreasing the height of each bar. + +1. Calculate the new bar height based on the padded height. + + ```ts + const padding = 20; + const chartHeight = height - padding; + const barHeight = chartHeight / values.length; + ``` + +1. Translate the axis by adding a transform to the `g` element. + + ```ts + { + d3.select(node).call(axis as any); + }} + /> + ``` + +Congrats! You've created a simple and responsive bar chart. + +## Complete example + +```ts +import React from 'react'; +import { PanelProps } from '@grafana/data'; +import { SimpleOptions } from 'types'; +import { useTheme } from '@grafana/ui'; +import * as d3 from 'd3'; + +interface Props extends PanelProps {} + +export const SimplePanel: React.FC = ({ options, data, width, height }) => { + const theme = useTheme(); + + const values = [4, 8, 15, 16, 23, 42]; + + const scale = d3 + .scaleLinear() + .domain([0, d3.max(values) || 0.0]) + .range([0, width]); + + const axis = d3.axisBottom(scale); + + const padding = 20; + const chartHeight = height - padding; + const barHeight = chartHeight / values.length; + + return ( + + + {values.map((value, i) => ( + + ))} + + { + d3.select(node).call(axis as any); + }} + /> + + ); +}; +``` + +## Summary + +In this tutorial you built a panel plugin with D3.js. diff --git a/docs/sources/tutorials/build-a-panel-plugin/index.md b/docs/sources/tutorials/build-a-panel-plugin/index.md new file mode 100644 index 00000000000..4f5f101d000 --- /dev/null +++ b/docs/sources/tutorials/build-a-panel-plugin/index.md @@ -0,0 +1,259 @@ +--- +title: Build a panel plugin +summary: Learn how to create a custom visualization for your dashboards. +description: Learn how to create a custom visualization for your dashboards. +id: build-a-panel-plugin +categories: ['plugins'] +tags: ['beginner'] +status: Published +authors: ['grafana_labs'] +Feedback Link: https://github.com/grafana/tutorials/issues/new +weight: 50 +--- + +## Introduction + +Panels are the building blocks of Grafana. They allow you to visualize data in different ways. While Grafana has several types of panels already built-in, you can also build your own panel, to add support for other visualizations. + +For more information about panels, refer to the documentation on [Panels](/docs/grafana/latest/panels/). + +{{% class "prerequisite-section" %}} + +### Prerequisites + +- Grafana >=7.0 +- NodeJS >=14 +- yarn + {{% /class %}} + +## Set up your environment + +{{< docs/shared lookup="tutorials/set-up-environment.md" source="grafana" version="latest" >}} + +## Create a new plugin + +{{< docs/shared lookup="tutorials/create-plugin.md" source="grafana" version="latest" >}} + +## Anatomy of a plugin + +{{< docs/shared lookup="tutorials/plugin-anatomy.md" source="grafana" version="latest" >}} + +## Panel plugins + +Since Grafana 6.x, panels are [ReactJS components](https://reactjs.org/docs/components-and-props.html). + +Prior to Grafana 6.0, plugins were written in [AngularJS](https://angular.io/). Even though we still support plugins written in AngularJS, we highly recommend that you write new plugins using ReactJS. + +### Panel properties + +The [PanelProps](https://github.com/grafana/grafana/blob/747b546c260f9a448e2cb56319f796d0301f4bb9/packages/grafana-data/src/types/panel.ts#L27-L40) interface exposes runtime information about the panel, such as panel dimensions, and the current time range. + +You can access the panel properties through `props`, as seen in your plugin. + +**src/SimplePanel.tsx** + +```js +const { options, data, width, height } = props; +``` + +### Development workflow + +Next, you'll learn the basic workflow of making a change to your panel, building it, and reloading Grafana to reflect the changes you made. + +First, you need to add your panel to a dashboard: + +1. Open Grafana in your browser. +1. Create a new dashboard, and add a new panel. +1. Select your panel from the list of visualization types. +1. Save the dashboard. + +Now that you can view your panel, try making a change to the panel plugin: + +1. In `SimplePanel.tsx`, change the fill color of the circle. +1. Run `yarn dev` to build the plugin. +1. In the browser, reload Grafana with the new changes. + +## Add panel options + +Sometimes you want to offer the users of your panel an option to configure the behavior of your plugin. By configuring _panel options_ for your plugin, your panel will be able to accept user input. + +In the previous step, you changed the fill color of the circle in the code. Let's change the code so that the plugin user can configure the color from the panel editor. + +#### Add an option + +Panel options are defined in a _panel options object_. `SimpleOptions` is an interface that describes the options object. + +1. In `types.ts`, add a `CircleColor` type to hold the colors the users can choose from: + + ``` + type CircleColor = 'red' | 'green' | 'blue'; + ``` + +1. In the `SimpleOptions` interface, add a new option called `color`: + + ``` + color: CircleColor; + ``` + +Here's the updated options definition: + +**src/types.ts** + +```ts +type SeriesSize = 'sm' | 'md' | 'lg'; +type CircleColor = 'red' | 'green' | 'blue'; + +// interface defining panel options type +export interface SimpleOptions { + text: string; + showSeriesCount: boolean; + seriesCountSize: SeriesSize; + color: CircleColor; +} +``` + +#### Add an option control + +To change the option from the panel editor, you need to bind the `color` option to an _option control_. + +Grafana supports a range of option controls, such as text inputs, switches, and radio groups. + +Let's create a radio control and bind it to the `color` option. + +1. In `src/module.ts`, add the control at the end of the builder: + + ```ts + .addRadio({ + path: 'color', + name: 'Circle color', + defaultValue: 'red', + settings: { + options: [ + { + value: 'red', + label: 'Red', + }, + { + value: 'green', + label: 'Green', + }, + { + value: 'blue', + label: 'Blue', + }, + ], + } + }); + ``` + + The `path` is used to bind the control to an option. You can bind a control to nested option by specifying the full path within a options object, for example `colors.background`. + +Grafana builds an options editor for you and displays it in the panel editor sidebar in the **Display** section. + +#### Use the new option + +You're almost done. You've added a new option and a corresponding control to change the value. But the plugin isn't using the option yet. Let's change that. + +1. To convert option value to the colors used by the current theme, add a `switch` statement right before the `return` statement in `SimplePanel.tsx`. + + **src/SimplePanel.tsx** + + ```ts + let color: string; + switch (options.color) { + case 'red': + color = theme.palette.redBase; + break; + case 'green': + color = theme.palette.greenBase; + break; + case 'blue': + color = theme.palette.blue95; + break; + } + ``` + +1. Configure the circle to use the color. + + ```ts + + + + ``` + +Now, when you change the color in the panel editor, the fill color of the circle changes as well. + +## Create dynamic panels using data frames + +Most panels visualize dynamic data from a Grafana data source. In this step, you'll create one circle per series, each with a radius equal to the last value in the series. + +> To use data from queries in your panel, you need to set up a data source. If you don't have one available, you can use the [TestData DB](/docs/grafana/latest/features/datasources/testdata) data source while developing. + +The results from a data source query within your panel are available in the `data` property inside your panel component. + +```ts +const { data } = props; +``` + +`data.series` contains the series returned from a data source query. Each series is represented as a data structure called _data frame_. A data frame resembles a table, where data is stored by columns, or _fields_, instead of rows. Every value in a field share the same data type, such as string, number, or time. + +Here's an example of a data frame with a time field, `Time`, and a number field, `Value`: + +| Time | Value | +| ------------- | ----- | +| 1589189388597 | 32.4 | +| 1589189406480 | 27.2 | +| 1589189513721 | 15.0 | + +Let's see how you can retrieve data from a data frame and use it in your visualization. + +1. Get the last value of each field of type `number`, by adding the following to `SimplePanel.tsx`, before the `return` statement: + + ```ts + const radii = data.series + .map((series) => series.fields.find((field) => field.type === 'number')) + .map((field) => field?.values.get(field.values.length - 1)); + ``` + + `radii` will contain the last values in each of the series that are returned from a data source query. You'll use these to set the radius for each circle. + +1. Change the `svg` element to the following: + + ```ts + + + {radii.map((radius, index) => { + const step = width / radii.length; + return ; + })} + + + ``` + + Note how we're creating a `` element for each value in `radii`: + + ```ts + { + radii.map((radius, index) => { + const step = width / radii.length; + return ; + }); + } + ``` + + We use the `transform` here to distribute the circle horizontally within the panel. + +1. Rebuild your plugin and try it out by adding multiple queries to the panel. Refresh the dashboard. + +If you want to know more about data frames, check out our introduction to [Data frames](/docs/grafana/latest/developers/plugins/data-frames/). + +## Summary + +In this tutorial you learned how to create a custom visualization for your dashboards. diff --git a/docs/sources/tutorials/build-a-streaming-data-source-plugin/index.md b/docs/sources/tutorials/build-a-streaming-data-source-plugin/index.md new file mode 100644 index 00000000000..7978f6e3629 --- /dev/null +++ b/docs/sources/tutorials/build-a-streaming-data-source-plugin/index.md @@ -0,0 +1,164 @@ +--- +title: Build a streaming data source backend plugin +summary: Create a backend for your data source plugin with streaming capabilities. +description: Create a backend for your data source plugin with streaming capabilities. +id: build-a-streaming-data-source-backend-plugin +categories: ['plugins'] +tags: ['beginner'] +status: Published +authors: ['grafana_labs'] +Feedback Link: https://github.com/grafana/tutorials/issues/new +weight: 75 +--- + +## Introduction + +Grafana supports a wide range of data sources, including Prometheus, MySQL, and even Datadog. In previous tutorials we have shown how to extend Grafana capabilities to query custom data sources by [building a backend datasource plugin](/tutorials/build-a-data-source-backend-plugin/). In this tutorial we take a step further and add streaming capabilities to the backend datasource plugin. Streaming allows plugins to push data to Grafana panels as soon as data appears (without periodic polling from UI side). + +For more information about backend plugins, refer to the documentation on [Backend plugins](/docs/grafana/latest/developers/plugins/backend/). + +In this tutorial, you'll: + +- Extend a backend plugin with streaming capabilities + +{{% class "prerequisite-section" %}} + +#### Prerequisites + +- Knowledge about how data sources are implemented in the frontend. +- Knowledge about [backend datasource anatomy](/tutorials/build-a-data-source-backend-plugin/) +- Grafana 8.0+ +- Go ([Version](https://github.com/grafana/plugin-tools/blob/main/packages/create-plugin/templates/backend/go.mod#L3)) +- [Mage](https://magefile.org/) +- NodeJS ([Version](https://github.com/grafana/plugin-tools/blob/main/packages/create-plugin/templates/common/package.json#L66)) +- yarn + {{% /class %}} + +## Set up your environment + +{{< docs/shared lookup="tutorials/set-up-environment.md" source="grafana" version="latest" >}} + +## Create a new plugin + +To build a backend for your data source plugin, Grafana requires a binary that it can execute when it loads the plugin during start-up. In this guide, we will build a binary using the [Grafana plugin SDK for Go](/docs/grafana/latest/developers/plugins/backend/grafana-plugin-sdk-for-go/). + +The easiest way to get started is to use the Grafana [create-plugin tool](https://www.npmjs.com/package/@grafana/create-plugin). Navigate to the plugin folder that you configured in step 1 and type: + +``` +npx @grafana/create-plugin +``` + +Follow the steps and select **datasource** as your plugin type and answer **yes** when prompted to create a backend for your plugin. + +```bash +cd my-plugin +``` + +Install frontend dependencies and build frontend parts of the plugin to _dist_ directory: + +```bash +yarn install +yarn build +``` + +Run the following to update [Grafana plugin SDK for Go](/docs/grafana/latest/developers/plugins/backend/grafana-plugin-sdk-for-go/) dependency to the latest minor version: + +```bash +go get -u github.com/grafana/grafana-plugin-sdk-go +go mod tidy +``` + +Build backend plugin binaries for Linux, Windows and Darwin to _dist_ directory: + +```bash +mage -v +``` + +Now, let's verify that the plugin you've built can be used in Grafana when creating a new data source: + +1. Restart your Grafana instance. +1. Open Grafana in your web browser. +1. Navigate via the side-menu to **Configuration** -> **Data Sources**. +1. Click **Add data source**. +1. Find your newly created plugin and select it. +1. Enter a name and then click **Save & Test** (ignore any errors reported for now). + +You now have a new data source instance of your plugin that is ready to use in a dashboard. To confirm, follow these steps: + +1. Navigate via the side-menu to **Create** -> **Dashboard**. +1. Click **Add new panel**. +1. In the query tab, select the data source you just created. +1. A line graph is rendered with one series consisting of two data points. +1. Save the dashboard. + +### Troubleshooting + +#### Grafana doesn't load my plugin + +By default, Grafana requires backend plugins to be signed. To load unsigned backend plugins, you need to +configure Grafana to [allow unsigned plugins](/docs/grafana/latest/plugins/plugin-signature-verification/#allow-unsigned-plugins). +For more information, refer to [Plugin signature verification](/docs/grafana/latest/plugins/plugin-signature-verification/#backend-plugins). + +## Anatomy of a backend plugin + +As you may notice till this moment we did the same steps described in [build a backend datasource plugin tutorial](/tutorials/build-a-data-source-backend-plugin/). At this point, you should be familiar with backend plugin structure and a way how data querying and health check capabilities could be implemented. Let's take the next step and discuss how datasource plugin can handle data streaming. + +## Add streaming capabilities + +What we want to achieve here is to issue a query to load initial data from a datasource plugin and then switching to data streaming mode where the plugin will push data frames to Grafana time-series panel. + +In short – implementing a streaming plugin means implementing a `backend.StreamHandler` interface which contains `SubscribeStream`, `RunStream`, and `PublishStream` methods. + +`SubscribeStream` is a method where the plugin has a chance to authorize user subscription requests to a channel. Users on the frontend side subscribe to different channels to consume real-time data. + +When returning a `data.Frame` with initial data we can return a special field `Channel` to let the frontend know that we are going to stream data frames after initial data load. When the frontend receives a frame with a `Channel` set it automatically issues a subscription request to that channel. + +Channel is a string identifier of topic to which clients can subscribe in Grafana Live. See a documentation of Grafana Live for [details about channel structure](/docs/grafana/latest/live/live-channel/). + +As said in docs in Grafana Live channel consists of 3 parts delimited by `/`: + +- Scope +- Namespace +- Path + +For datasource plugin channels Grafana uses `ds` scope. Namespace in the case of datasource channels is a datasource unique ID (UID) which is issued by Grafana at the moment of datasource creation. The path is a custom string that plugin authors free to choose themselves (just make sure it consists of allowed symbols). I.e. datasource channel looks like `ds//`. + +So to let the frontend know that we are going to stream data we set a `Channel` field into frame metadata inside `QueryData` implementation. In our tutorial it's a `ds//stream`. The frontend will issue a subscription request to this channel. + +Inside `SubscribeStream` implementation we check whether a user allowed to subscribe on a channel path. If yes – we return an OK status code to tell Grafana user can join a channel: + +```go +status := backend.SubscribeStreamStatusPermissionDenied +if req.Path == "stream" { + // Allow subscribing only on expected path. + status = backend.SubscribeStreamStatusOK +} +return &backend.SubscribeStreamResponse{ + Status: status, +}, nil +``` + +As soon as the first subscriber joins a channel Grafana opens a unidirectional stream to consume streaming frames from a plugin. To handle this and to push data towards clients we implement a `RunStream` method which provides a way to push JSON data into a channel. So we can push data frame like this (error handling skipped): + +```go +// Send frame to stream including both frame schema and data frame parts. +_ = sender.SendFrame(frame, data.IncludeAll) +``` + +Open example datasource query editor and make sure `With Streaming` toggle is on. After doing this you should see data displayed and then periodically updated by streaming frames coming periodically from `RunStream` method. + +The important thing to note is that Grafana opens a unidirectional stream only once per channel upon the first subscriber joined. Every other subscription request will be still authorized by `SubscribeStream` method but the new `RunStream` won't be issued. I.e. you can have many active subscribers but only one running stream. At this moment this guarantee works for a single Grafana instance, we are planning to support this for highly-available Grafana setup (many Grafana instances behind load-balancer) in future releases. + +The stream will be automatically closed as soon as all subscriber users left. + +For the tutorial use case, we only need to properly implement `SubscribeStream` and `RunStream` - we don't need to handle publications to a channel from users. But we still need to write `PublishStream` method to fully implement `backend.StreamHandler` interface. Inside `PublishStream` we just do not allow any publications from users since we are pushing data from a backend: + +```go +return &backend.PublishStreamResponse{ + Status: backend.PublishStreamStatusPermissionDenied, +}, nil +``` + +## Summary + +In this tutorial you created a backend for your data source plugin with streaming capabilities. diff --git a/docs/sources/tutorials/build-an-app-plugin/index.md b/docs/sources/tutorials/build-an-app-plugin/index.md new file mode 100644 index 00000000000..0649b0fd596 --- /dev/null +++ b/docs/sources/tutorials/build-an-app-plugin/index.md @@ -0,0 +1,208 @@ +--- +title: Build an app plugin +summary: Learn at how to create an app for Grafana. +description: Learn at how to create an app for Grafana. +id: build-an-app-plugin +categories: ['plugins'] +tags: ['beginner'] +status: Published +authors: ['grafana_labs'] +Feedback Link: https://github.com/grafana/tutorials/issues/new +weight: 50 +draft: true +--- + +## Introduction + +App plugins are Grafana plugins that can bundle data source and panel plugins within one package. They also let you create _custom pages_ within Grafana. Custom pages enable the plugin author to include things like documentation, sign-up forms, or to control other services over HTTP. + +Data source and panel plugins will show up like normal plugins. The app pages will be available in the main menu. + +{{% class "prerequisite-section" %}} + +### Prerequisites + +- Grafana 7.0 +- NodeJS 12.x +- yarn + {{% /class %}} + +## Set up your environment + +{{< docs/shared lookup="tutorials/set-up-environment.md" source="grafana" version="latest" >}} + +## Create a new plugin + +{{< docs/shared lookup="tutorials/create-plugin.md" source="grafana" version="latest" >}} + +## Anatomy of a plugin + +{{< docs/shared lookup="tutorials/plugin-anatomy.md" source="grafana" version="latest" >}} + +## App plugins + +App plugins let you bundle resources such as dashboards, panels, and data sources into a single plugin. + +Any resource you want to include needs to be added to the `includes` property in the `plugin.json` file. To add a resource to your app plugin, you need to include it to the `plugin.json`. + +Plugins that are included in an app plugin are available like any other plugin. + +Dashboards and pages can be added to the app menu by setting `addToNav` to `true`. + +By setting `"defaultNav": true`, users can navigate to the dashboard by clicking the app icon in the side menu. + +## Add a custom page + +App plugins let you extend the Grafana user interface through the use of _custom pages_. + +Any requests sent to `/a/`, e.g. `/a/myorgid-simple-app/`, are routed to the _root page_ of the app plugin. The root page is a React component that returns the content for a given route. + +While you're free to implement your own routing, in this tutorial you'll use a tab-based navigation page that you can use by calling `onNavChange`. + +Let's add a tab for managing server instances. + +1. In the `src/pages` directory, add a new file called `Instances.tsx`. This component contains the content for the new tab. + + ```ts + import { AppRootProps } from '@grafana/data'; + import React, { FC } from 'react'; + + export const Instances: FC = ({ query, path, meta }) => { + return

Hello

; + }; + ``` + +1. Register the page by adding it to the `pages` array in `src/pages/index.ts`. + + **index.ts** + + ```ts + import { Instances } from './Instances'; + ``` + + ```ts + { + component: Instances, + icon: 'file-alt', + id: 'instances', + text: 'Instances', + } + ``` + +1. Add the page to the app menu, by including it in `plugin.json`. This will be the main view of the app, so we'll set `defaultNav` to let users quickly get to it by clicking the app icon in the side menu. + + **plugin.json** + + ```json + "includes": [ + { + "type": "page", + "name": "Instances", + "path": "/a/myorgid-simple-app?tab=instances", + "role": "Viewer", + "addToNav": true, + "defaultNav": true + } + ] + ``` + +> **Note:** While `page` includes typically reference pages created by the app, you can set `path` to any URL, internal or external. Try setting `path` to `https://grafana.com`. + +## Configure the app + +Let's add a new configuration page where users are able to configure default zone and regions for any instances they create. + +1. In `module.ts`, add new configuration page using the `addConfigPage` method. `body` is the React component that renders the page content. + + **module.ts** + + ```ts + .addConfigPage({ + title: 'Defaults', + icon: 'fa fa-info', + body: DefaultsConfigPage, + id: 'defaults', + }) + ``` + +## Add a dashboard + +#### Include a dashboard in your app + +1. In `src/`, create a new directory called `dashboards`. +1. Create a file called `overview.json` in the `dashboards` directory. +1. Copy the JSON definition for the dashboard you want to include and paste it into `overview.json`. If you don't have one available, you can find a sample dashboard at the end of this step. +1. In `plugin.json`, add the following object to the `includes` property. + + - The `name` of the dashboard needs to be the same as the `title` in the dashboard JSON model. + - `path` points out the file that contains the dashboard definition, relative to the `plugin.json` file. + + ```json + "includes": [ + { + "type": "dashboard", + "name": "System overview", + "path": "dashboards/overview.json", + "addToNav": true + } + ] + ``` + +1. Save and restart Grafana to load the new changes. + +## Bundle a plugin + +An app plugin can contain panel and data source plugins that get installed along with the app plugin. + +In this step, you'll add a data source to your app plugin. You can add panel plugins the same way by changing `datasource` to `panel`. + +1. In `src/`, create a new directory called `datasources`. +1. Create a new data source using Grafana create-plugin tool in a temporary directory. + + ```bash + mkdir tmp + cd tmp + npx @grafana/create-plugin + ``` + +1. Move the `src` directory in the data source plugin to `src/datasources`, and rename it to `my-datasource`. + + ```bash + mv ./my-datasource/src ../src/datasources/my-datasource + ``` + +Any bundled plugins are built along with the app plugin. Grafana looks for any subdirectory containing a `plugin.json` file and attempts to load a plugin in that directory. + +To let users know that your plugin bundles other plugins, you can optionally display it on the plugin configuration page. This is not done automatically, so you need to add it to the `plugin.json`. + +1. Include the data source in the `plugin.json`. The `name` property is only used for displaying in the Grafana UI. + + ```json + "includes": [ + { + "type": "datasource", + "name": "My data source" + } + ] + ``` + +#### Include external plugins + +If you want to let users know that your app requires an existing plugin, you can add it as a dependency in `plugin.json`. Note that they'll still need to install it themselves. + +```json +"dependencies": { + "plugins": [ + { + "type": "panel", + "name": "Worldmap Panel", + "id": "grafana-worldmap-panel", + "version": "^0.3.2" + } + ] +} +``` + +## Summary + +In this tutorial you learned how to create an app plugin. diff --git a/docs/sources/tutorials/create-alerts-from-flux-queries/index.md b/docs/sources/tutorials/create-alerts-from-flux-queries/index.md new file mode 100644 index 00000000000..405b4f861cd --- /dev/null +++ b/docs/sources/tutorials/create-alerts-from-flux-queries/index.md @@ -0,0 +1,331 @@ +--- +title: How to create Grafana alerts with InfluxDB and the Flux query language +summary: Create complex alerts from Flux queries in the new Grafana Alerting +description: Create complex alerts from Flux queries in the new Grafana Alerting +id: grafana-alerts-flux-queries +categories: ['alerting'] +tags: ['advanced'] +status: published +authors: ['grant_pinkos'] +Feedback Link: https://github.com/grafana/tutorials/issues/new +weight: 70 +--- + +# How to create Grafana alerts with InfluxDB and the Flux query language + +[Grafana Alerting](/docs/grafana/latest/alerting/) represents a powerful new approach to systems observability and incident response management. While the alerting platform is perhaps best known for its strong integrations with Prometheus, the system works with numerous popular data sources including InfluxDB. In this tutorial we will learn how to create Grafana alerts using InfluxDB and the newer Flux query language. We will cover five common scenarios from the most basic to the most complex. Together, these five scenarios will provide an excellent guide for almost any type of alerting query that you wish to create using Grafana and Flux. + +Before we dive into our alerting scenarios, it is worth considering the development of InfluxDB's two popular query languages: InfluxQL and Flux. Originally, InfluxDB used [InfluxQL](https://docs.influxdata.com/influxdb/v2.5/reference/syntax/influxql/spec/) as their query language, which uses a SQL-like syntax. But beginning with InfluxDB v1.8, the company introduced [Flux](https://docs.influxdata.com/flux/v0.x/), "an open source functional data scripting language designed for querying, analyzing, and acting on data." "Flux," its official documentation goes on to state, "unifies code for querying, processing, writing, and acting on data into a single syntax. The language is designed to be usable, readable, flexible, composable, testable, contributable, and shareable." + +In the following five examples we will see just how powerful and flexible the new Flux query language can be. We will also see just how well Flux pairs with Grafana Alerting. + +## Example 1: Create an alert when a value is above or below a set threshold + +Our first example uses a common real-world scenario for InfluxDB and Grafana Alerting. Popular with IoT and edge applications, InfluxDB excels at on-site, real-time observability. In this example, and in fact for many of the following examples, we will consider the hypothetical scenario where we are monitoring a number of fluid tanks in a manufacturing plant. This scenario, [based on an actual application of InfluxDB and Alerting](/go/grafanaconline/2021/plant-efficiency-grafana-cloud/), will allow us to work through Grafana's various alerting setups, progressing from the simplest to the most complex. + +For Example 1, let's consider the following scenario: we are monitoring one tank, `A5`, for which we are storing real-time temperature data. We need to make sure that the temperature in this tank is always greater than 30 °C and less than 60 °C. + +We want to write a Grafana alert that will trigger whenever the temperature in tank `A5` crosses the lower threshold of 30 °C or the upper threshold of 60 °C. + +To do this, we'll: 1. create a Grafana alert rule. 1. add a Flux query. 1. add expressions to the alert rule. + +### Create a Grafana Alert rule + +1. Open the Grafana alerting menu and select **Alert rules**. +1. Click **New alert rule**. +1. Give your alert rule a name and then select **Grafana managed alert**. + For InfluxDB, you will always create a [Grafana managed rule](/docs/grafana/latest/alerting/alerting-rules/create-grafana-managed-rule/#add-grafana-managed-rule). + +### Add an initial Flux query to the alert rule + +Still in the **Step 2** section of the Alert rule page, you will see three boxes: a query editor (`A`), and then two sections labelled `B` and `C`. You will use these three sections to construct your rule. Let's move through them one by one. + +First, we want to query the data in our imaginary InfluxDB instance to obtain a time series graph of the temperature of tank A5. For this you would choose your InfluxDB data source from the dropdown and then write a query like this: + + ``` + from(bucket: "RetroEncabulator") + |> range(start: v.timeRangeStart, stop: v.timeRangeStop) + |> filter(fn: (r) => r["_measurement"] == "TemperatureData") + |> filter(fn: (r) => r["Tank"] == "A5") + |> filter(fn: (r) => r["_field"] == "Temperature") + |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false) + |> yield(name: "mean") + ``` + +This is a fairly typical Flux query. Let's go through it function by function. We begin using [the `from()` function](https://docs.influxdata.com/flux/v0.x/stdlib/influxdata/influxdb/from/) to choose the correct bucket where our tank data resides. Then we use [a `range()` function](https://docs.influxdata.com/flux/v0.x/stdlib/universe/range/) to filter our rows based on time constraints. Then we pass our data through three [`filter()` functions](https://docs.influxdata.com/flux/v0.x/stdlib/universe/filter/) to narrow our results. We choose a specific [`measurement` (a special keyword in InfluxDB)](https://docs.influxdata.com/influxdb/v1.8/concepts/glossary/#measurement), then our tank in question (`A5`), and then a specific [`field` (another special keyword in InfluxDB)](https://docs.influxdata.com/influxdb/v1.8/concepts/glossary/#field). After this we pass the data into [an `aggregateWindow()` function](https://docs.influxdata.com/flux/v0.x/stdlib/universe/aggregatewindow/), which downsamples our data into specific periods of time, and then finally [a `yield()` function](https://docs.influxdata.com/flux/v0.x/stdlib/universe/yield/), which specifies which final result we want: `mean`. + +This Flux query will yield a time-series graph like this: + +![grafana alerts from flux queries](/media/tutorials/screenshot-flux-timeseries-graph.png) + +### Add expressions to your Grafana Alert rule + +With data now appearing in our rule setup, our next step is to create an [expression](/docs/grafana/v9.0/panels/query-a-data-source/use-expressions-to-manipulate-data/about-expressions/#using-expressions). Move to section `B`. For this scenario, we want to create a Reduce expression that will reduce the above to a single value. In this image, you can see that we have chosen to reduce our time-series data the `Last` value from input `A`. In this case, it returns a value 53 degrees celsius for Tank A5: + +![grafana alerts from flux queries](/media/tutorials/screenshot-flux-reduce-expression.png) + +Finally, we need to create a math expression that Grafana will alert on. In our case we will write an expression with two conditions separated by the OR `||` operator. We want to trigger an alert any time our result in section `B` is less than 30 or more than 60. This looks like `$B < 30 || $B > 60`: + +![grafana alerts from flux queries](/media/tutorials/screenshot-flux-math-expression.png) + +Set the alert condition to `C - expression`. We can now preview our alert. Here is a preview of this alert when the state is `Normal`: + +![grafana alerts from flux queries](/media/tutorials/screenshot-flux-alert-preview-state-normal.png) + +And here is a preview of this alert when the state is `Alerting`: + +![grafana alerts from flux queries](/media/tutorials/screenshot-flux-alert-alert-preview-state-alerting.png) + +Note that the Reduce expression above is needed. Without it, when previewing the results, Grafana would display `invalid format of evaluation results for the alert definition B: looks like time series data, only reduced data can be alerted on`. + +💡Tip: In case your locale is still stubbornly using Fahrenheit, we can modify the above Flux query by adding (before the aggregateWindow statement) a map() function to to convert (or map) the values from °C to °F. Note that we are not creating a new field. We are simply remapping the existing value. + +```flux +|> map(fn: (r) => ({r with _value: r._value * 1.8 + 32.0})) +``` + +### Conclusion + +Using these three steps you can create a Flux-based Grafana Alert that will trigger on either of two thresholds from a single data source. But what if you need to trigger an alert based on **multiple conditions and from multiple time-series**? In example two we will cover this very scenario. + +## Example 2: how to create a Grafana alert from two queries and two conditions + +Let's mix things up a bit for example two and leave our imaginary manufacturing plant. Imagine you're an assistant to the great Dr. Emmett Brown from Back to the Future, and Doc has tasked you with the following challenge: "I want an alert sent to me every time both conditions for time travel are met: when the velocity of a vehicle reaches 88 miles per hour and an object generates 1.21 jigowatts of electricity." + +Let's assume we are tracking this data in InfluxDB and Grafana. Let's also assume that each of the above data sources comes from different buckets. How do we alert on this? How do we use Grafana and Flux to alert on two distinct conditions originating from two distinct data sources? + +### Add two Flux queries to your Grafana Alert rule + +Like we did in example 1, let's first mock up our queries. Our query for our vehicle data is very similar to our last query. We use a `from()`, `range()`, and a sequence of `filter()` functions. We then use `AggregateWindow()` and `yield()` to narrow our data even more. In this case, the result is a time series tracking the velocity of our 1983 DeLorean: + +```flux +from(bucket: "vehicles") +|> range(start: v.timeRangeStart, stop: v.timeRangeStop) +|> filter(fn: (r) => r["_measurement"] == "VehicleData") +|> filter(fn: (r) => r["VehicleType"] == "DeLorean") +|> filter(fn: (r) => r["VehicleYear"] == "1983") +|> filter(fn: (r) => r["_field"] == "velocity") +|> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false) +|> yield(name: "mean") +``` + +Our second query will trigger an alert whenever our electricity resource (the lightning strike on the Hill Valley clocktower) reaches the needed 1.21 jigowatts. A query like this would look very similar to our vehicle velocity query: + +```flux +from(bucket: "HillValley") +|> range(start: v.timeRangeStart, stop: v.timeRangeStop) +|> filter(fn: (r) => r["_measurement"] == "ElectricityData") +|> filter(fn: (r) => r["Location"] == "clocktower") +|> filter(fn: (r) => r["Source"] == "lightning") +|> filter(fn: (r) => r["_field"] == "power") +|> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false) +|> yield(name: "mean") +``` + +We are now ready to modify this data using expressions. + +### Add expressions to your Grafana Alert rule + +1. Let's now use the same steps to reduce each query to the last (most recent) value. Reducing Query `A` to a single value might look like this: + + ![grafana alerts from flux queries](/media/tutorials/screenshot-flux-additional-queries-reduce-expression-A.png) + +1. And here we are reducing query `B`: + + ![grafana alerts from flux queries](/media/tutorials/screenshot-flux-additional-queries-reduce-expression-B.png) + +1. Now, in section `C` we need to create a math expression to be alerted on. In this case we will use the AND `&&` operator to specify that two conditions must be met: the value of `C` (the reduced value from query `A`) must be greater than 88.0 while the value of `D` (the reduced value from query `B`) must be greater than 1.21. We write this as `$C > 88.0 && $D > 1.21` + + ![grafana alerts from flux queries](/media/tutorials/screenshot-flux-additional-queries-math-expression.png) + +And here is a preview of our alerts: + +![grafana alerts from flux queries](https://raw.githubusercontent.com/grafana/tutorials/master/content/tutorials/assets/flux-additional-queries-alert-preview.png) + +💡Tip: If your data in InfluxDB happens to have an unnecessarily large number of digits to the right of the decimal (such as 1.2104705741732575 shown above), and you want your Grafana alerts to be more legible, try using {{ printf "%.2f" $values.D.Value }}. For example, in the annotation Summary, we could write the following: + +``` +{{ $values.D.Labels.Source }} at the {{ $values.D.Labels.Location }} has generated {{ printf "%.2f" $values.D.Value }} jigowatts.` +``` + +This will display as follows: +![grafana alerts from flux queries](/media/tutorials/screenshot-flux-tip-significant-figures.png)) + +You can reference our documentation on [alert message templating](/docs/grafana/latest/alerting/contact-points/message-templating/) to learn more about this powerful feature. + +### Conclusion + +In this example we showed how to create a Flux-based alert that uses two distinct conditions from two distinct queries that use data from two distinct data sources. For example three we will switch gears and tackle another popular alerting scenario: how to create an alert based on an aggregated (per day) value. + +## Example 3: how to create a Grafana Alert based on an aggregated (per-day) value + +One of the most common requests in [Grafana's community forum](https://community.grafana.com) involves graphing daily electrical consumption and production. This sort of data is very often stored in InfluxDB. In this example we will see how to aggregate time series data into a per-day value and then alert on it. + +Let’s assume our electricity meter sends a reading to InfluxDB once per hour and contains the total kWh used for that hour. We want to write a query that will aggregate these per-hour values into a per-day value, then create an alert that triggers when the power consumption (kWh) exceeds 5,000 kWh per day. + +### Add an initial Flux query to your Grafana Alert rule + +1. Let's begin by examining a typical query and the resulting time graph for our hourly data across a 7-day period. A query like this is shown below: + + ```flux + from(bucket: "RetroEncabulator") + |> range(start: v.timeRangeStart, stop: v.timeRangeStop) + |> filter(fn: (r) => r["_measurement"] == "ElectricityData") + |> filter(fn: (r) => r["Location"] == "PlantD5") + |> filter(fn: (r) => r["_field"] == "power_consumed") + |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false) + |> yield(name: "power") + ``` + + We can see the same pattern of Flux functions here that we say in examples 1 and 2. A query like this would produce a graph similar to the following: + + ![grafana alerts from flux queries](/media/tutorials/screenshot-flux-aggregatewindow-timeseries-graph.png) + +1. Now let's adjust our query to calculate daily usage. With many datasources, this can be a rather complex operation. But with Flux, by simply changing the aggregateWindow function parameters we can calculate the daily usage over the same 7-day period: + + ```flux + from(bucket: "RetroEncabulator") + |> range(start: v.timeRangeStart, stop: v.timeRangeStop) + |> filter(fn: (r) => r["_measurement"] == "ElectricityData") + |> filter(fn: (r) => r["Location"] == "PlantD5") + |> filter(fn: (r) => r["_field"] == "power_consumed") + |> aggregateWindow(every: 1d, fn: sum) + |> yield(name: "power") + ``` + + Note how we've adjusted our `aggregateWindow()` function to `aggregateWindow(every: 1d, fn: sum)`. This results in a graph like so: + + ![grafana alerts from flux queries](/media/tutorials/screenshot-flux-aggregatewindow-aggregated.png) + +1. Add expressions to your Grafana Alert rule. + + Now that we have our per-day query correct, we can continue using the same pattern as before, adding expressions to reduce and perform math on our results. + + As before, let's reduce our query to a single value: + + ![grafana alerts from flux queries](/media/tutorials/screenshot-flux-aggregatewindow-reduce-expression.png) + + Now create a math expression to be alerted on and set the evaluation behavior. In this case we want to write `$B > 5000`: + + ![grafana alerts from flux queries](/media/tutorials/screenshot-flux-aggregatewindow-math-expression.png) + + And now we are alerting on our daily electricity consumption whenever we exceed 5000 kWh. Here is preview of our alert: + + ![grafana alerts from flux queries](/media/tutorials/screenshot-flux-aggregatewindow-alert-preview.png) + +### Conclusion + +Plotting and aggregating electrical consumption is a common use case for combining InfluxDB and Grafana. Using Flux, we saw just how easy it can be to group our data by day and then alert on that daily value. In our next two examples we will examine the more complex form of Grafana Alert: multidimensional alerts. + +## Example 4: create a dynamic (multidimensional) Grafana Alert using Flux + +Let’s return to our fluid tanks from example 1, but this time let’s assume we have 5 tanks (A5, B4, C3, D2, and E1). We are now tracking the temperature in five tanks: A5, B4, C3, D2, and E1. + +We want to create one multidimensional alert that will notify us whenever the temperature in any tank is less than 30 °C or greater than 60 °C. + +### Add an initial Flux query to your Grafana Alert rule + +We begin, as always, by writing our initial query. This is very similar to our query in example 1, but note how our third `filter()` function captures the data from all five tanks and not just `A5`: + +```flux +from(bucket: "HyperEncabulator") +|> range(start: v.timeRangeStart, stop: v.timeRangeStop) +|> filter(fn: (r) => r["_measurement"] == "TemperatureData") +|> filter(fn: (r) => r["MeasType"] == "actual") +|> filter(fn: (r) => r["Tank"] == "A5" or r["Tank"] == "B4" or r["Tank"] == "C3" or r["Tank"] == "D2" or r["Tank"] == "E1") +|> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false) +|> yield(name: "mean") +``` + +💡Tip: If the tanks were shut down every night from 23:00 to 07:00, they would possibly fall below the 30 °C threshold. If one did not want to receive alerts during those hours, one can use the Flux function hourSelection() which filters rows by time values in a specified hour range. + +```flux +|> hourSelection(start: 7, stop: 23)` +``` + +A query like the one above will produce a time series graph like this: + +![grafana alerts from flux queries](/media/tutorials/screenshot-flux-multidimensional-timeseries-graph.png) + +### Add expressions to your Grafana Alert rule + +1. We create a Reduce expression that will reduce the time series for each tank to a single value. This gives us five distinct temperatures: + + ![grafana alerts from flux queries](/media/tutorials/screenshot-flux-multidimensional-reduce-expression.png)) + +1. Create a math expression to be alerted on. This is the exact same expression from example 1, `$B < 30 || $B > 60`: + + ![grafana alerts from flux queries](/media/tutorials/screenshot-flux-multidimensional-math-expression.png) + +As we can see three tanks are within the acceptable thresholds while two tanks have crossed the upper boundary. This would trigger an alert for tanks `D2` and `E1`. + +### Conclusion + +With multidimensional alerts we can avoid repeating ourselves. But what if the scenario were even more complex? In the next and final example, we will examine how to use multidimensional alerts to create the most dynamic alerts possible. + +## Example 5: how to create a dynamic (multidimensional) Grafana Alert using multiple queries and multiple thresholds with Flux + +For this final example let's continue with our five fluid tanks and their five datasets.Let’s assume again that each tank has a temperature controller with a setpoint value that is stored in InfluxDB. Let’s mix things up and assume that each tank has a _different_ setpoint, where we always need to be within 3 degrees of the setpoint. + +We want to create one multidimensional alert that will cover each unique scenario for each tank, triggering an alert whenever any tank's temperature moves beyond its unique allowable range. + +To better visualize this challenge, here is a table representing our five tanks, their temperature setpoints, and their allowable range: + +| Tank | Setpoint | Allowable Range (±3) | +| ---- | -------- | -------------------- | +| A5 | 45 | 42 to 48 | +| B4 | 55 | 52 to 58 | +| C3 | 60 | 57 to 63 | +| D2 | 72 | 69 to 75 | +| E1 | 80 | 77 to 83 | + +With Grafana Alerting, we can create a single multidimensional rule to cover all 5 tanks, and we can use Flux to compare the setpoint and actual value for each tank. In other words, one multidimensional alert can monitor 5 separate tanks, each with different setpoints and actual values, but all with one common "allowable threshold" (i.e. a temperature difference of ±3 degrees). + +### Add an initial Flux query to your Grafana Alert rule + +Let's begin with our data query. It is similar to our past queries, only now more complex. We must add extra functions to get our data into the proper format, including a `pivot()`, `map()`, `rename()`, `keep()`, and `drop()` function: + +```flux +from(bucket: "HyperEncabulator") + |> range(start: v.timeRangeStart, stop: v.timeRangeStop) + |> filter(fn: (r) => r["_measurement"] == "TemperatureData") + |> filter(fn: (r) => r["MeasType"] == "actual" or r["MeasType"] == "setpoint") + |> filter(fn: (r) => r["Tank"] == "A5" or r["Tank"] == "B4" or r["Tank"] == "C3" or r["Tank"] == "D2" or r["Tank"] == "E1") + |> filter(fn: (r) => r["_field"] == "Temperature") + |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false) + |> pivot(rowKey:["_time"], columnKey: ["MeasType"], valueColumn: "_value") + |> map(fn: (r) => ({ r with _value: (r.setpoint - r.actual)})) + |> rename(columns: {_value: "difference"}) + |> keep(columns: ["_time", "difference", "Tank"]) + |> drop(columns: ["actual", "setpoint"]) + |> yield(name: "mean") +``` + +Note in the above that we are calculating the difference between the actual and the setpoint. The way Grafana parses the result from InfluxDB is that if a \_value column is found, it is assumed to be a time-series. The quick workaround is to add the following `rename()` function: + +```flux + |> rename(columns: {_value: "something"}) +``` + +The above query results in this time series: + +![grafana alerts from flux queries](/media/tutorials/screenshot-flux-complex-query-timeseries-graph.png) + +### Add expressions to your Grafana Alert rule + +1. Again, we create a Reduce expression for the above query to reduce each of the above to a single value. This value represents the temperature differential between each tank's setpoint and its actual real-time temperature: + + ![grafana alerts from flux queries](/media/tutorials/screenshot-flux-complex-query-reduce-expression.png) + +1. Now we create a math expression to be alerted on. This time we will create a condition that checks if the absolute value of our reduce calculation is greater than 3, `abs($(B))>3.0`: + + ![grafana alerts from flux queries](/media/tutorials/screenshot-flux-complex-query-math-expression.png) + +We can now see that two tanks, `D2` and `E1`, are evaluating to true. When we preview the alert we can see that those two tanks will trigger a notification and change their state from `Normal` to `Alerting`: + +![grafana alerts from flux queries](/media/tutorials/screenshot-flux-complex-query-alert-preview-state-normal.png) +![grafana alerts from flux queries](/media/tutorials/screenshot-flux-complex-query-alert-preview-state-alerting.png) + +### Conclusion + +Flux queries and Grafana Unified Alerting are a powerful combination to identify practically any alertable conditions in your dataset, or across your entire system. For more information on Grafana Alerting, [visit the documentation here](/docs/grafana/latest/alerting/). For more information on the Flux query language, [you can visit that documentation as well](https://docs.influxdata.com/flux/v0.x/). diff --git a/docs/sources/tutorials/create-users-and-teams/index.md b/docs/sources/tutorials/create-users-and-teams/index.md new file mode 100644 index 00000000000..23b02d1d663 --- /dev/null +++ b/docs/sources/tutorials/create-users-and-teams/index.md @@ -0,0 +1,236 @@ +--- +title: Create users and teams +summary: Learn how to set up teams and users. +description: Learn how to set up teams and users. +id: create-users-and-teams +categories: ['administration'] +tags: ['beginner'] +status: Published +authors: ['grafana_labs'] +Feedback Link: https://github.com/grafana/tutorials/issues/new +weight: 20 +--- + +## Introduction + +This tutorial is for admins or anyone that wants to learn how to manage +users in Grafana. You'll add multiple local users, organize them into teams, +and make sure they're only able to access the resources they need. + +### Scenario + +_Graphona_, a fictional telemarketing company, has asked you to configure Grafana +for their teams. + +In this scenario, you'll: + +- Create users and organize them into teams. +- Manage resource access for each user and team through roles and folders. + +{{% class "prerequisite-section" %}} + +### Prerequisites + +- Grafana 7.0 or newer, this tutorial was tested with Grafana 8.5. +- A user with the Admin or Server Admin role. + {{% /class %}} + +## Add users + +In Grafana, all users are granted an _organization role_ that determines what +resources they can access. + +There are three types of organization roles in Grafana. The **Grafana Admin** is +a global role, the default `admin` user has this role. + +- **Grafana Admin -** Manage organizations, users, and view server-wide settings. +- **Organization Administrator -** Manage data sources, teams, and users within an organization. +- **Editor -** Create and edit dashboards. +- **Viewer -** View dashboards. + +> **Note**: You can also configure Grafana to allow [anonymous access](/docs/grafana/latest/auth/overview/#anonymous-authentication), to make dashboards available even to those who don't have a Grafana user account. That's how Grafana Labs made https://play.grafana.org publicly available. + +### Exercise + +Graphona has asked you to add a group of early adopters that work in the Marketing and Engineering teams. They'll need to be able to edit their own team's dashboards, but want to have view access to dashboards that belong to the other team. + +| Name | Email | Username | +| ----------------- | ----------------------------- | ----------------- | +| Almaz Russom | almaz.russom@example.com | almaz.russom | +| Brenda Tilman | brenda.tilman@example.com | brenda.tilman | +| Mada Rawdha Tahan | mada.rawdha.tahan@example.com | mada.rawdha.tahan | +| Yuan Yang | yuan.yang@example.com | yuan.yang | + +#### Add users + +Repeat the following steps for each of the employees in the table above to create the new user accounts: + +1. Log in as a user that has the **Server Admin** role. +1. On the sidebar, click the **Server Admin** (shield) icon. +1. Choose **Users** from the menu drop-down, then click **New User**. +1. Enter the **Name**, **Email**, **Username**, and **Password** from the table above. +1. Click the **Create User** button to create the account. + +When you create a user they are granted the Viewer role by default, which means that they won't be able to make any changes to any of the resources in Grafana. That's ok for now, you'll grant more user permissions by adding users to _teams_ in the next step. + +## Assign users to teams + +Teams let you grant permissions to a group of users, instead of granting permissions to individual users one at a time. + +Teams are useful when onboarding new colleagues. When you add a user to a team, they get access to all resources assigned to that team. + +### Exercise + +In this step, you'll create two teams and assign users to them. + +| Username | Team | +| ----------------- | ----------- | +| brenda.tilman | Marketing | +| mada.rawdha.tahan | Marketing | +| almaz.russom | Engineering | +| yuan.yang | Engineering | + +#### Create a team + +Create the _Marketing_ and _Engineering_ teams. + +1. In the sidebar, hover your mouse over the **Configuration** (gear) icon and + then click **Teams**. +1. Click **New team**. +1. In **Name**, enter the name of the team: either _Marketing_ or _Engineering_. + You do not need to enter an email. +1. Click **Create**. +1. Click on the **Teams** link at the top of the page to return to teams page and create the second team. + +#### Add a user to a team + +Repeat these steps for each user to assign them to their team. Refer to the table above for team assignments. + +1. Click the team name _Marketing_ or _Engineering_ to add members to that team. +1. Click **Add member**. +1. In the **Add team member** box, click the drop-down arrow to choose the user you want to add to the team . +1. Click **Add to team**. + +When you're done, you'll have two teams with two users assigned to each. + +## Manage resource access with folders + +It's a good practice to use folders to organize collections of related dashboards. You can assign permissions at the folder level to individual users or teams. + +### Exercise + +The Marketing team is going to use Grafana for analytics, while the Engineering team wants to monitor the application they're building. + +You'll create two folders, _Analytics_ and _Application_, where each team can add their own dashboards. The teams still want to be able to view each other's dashboards. + +| Folder | Team | Permissions | +| ----------- | ----------- | ----------- | +| Analytics | Marketing | Edit | +| | Engineering | View | +| Application | Marketing | View | +| | Engineering | Edit | + +Repeat the following steps for each folder. You'll move through all three steps for each folder before moving on to the next one. + +#### Add a folder for each team + +1. In the sidebar, hover your cursor over the **Dashboards** (four squares) icon and then click **Browse**. +1. To create a folder, click **New Folder**. +1. In **Name**, enter the folder name. +1. Click **Create**. +1. Stay in the folder view and move on to the next sections to edit permissions for this folder. + +#### Remove the viewer role from folder permissions + +By default, when you create a folder, all users with the Viewer role are granted permission to view the folder. + +In this example, Graphona wants to explicitly grant teams access to folders. To support this, you need to remove the Viewer role from the list of permissions: + +1. Go to the **Permissions** tab. +1. Remove the Viewer role from the list, by clicking the red button on the right. +1. Stay in the permissions tab and move on to the next section to grant folder permissions for each team. + +#### Grant folder permissions to a team: + +1. Click **Add Permission**. +1. In the **Add Permission For** dialog, make sure "Team" is selected in the first box. +1. In the second box, select the team to grant access to. +1. In the third box, select the access you want to grant. +1. Click **Save**. +1. Repeat for the other team. +1. Click the **Dashboards** link at the top of the page to return to the dashboard list. + +When you're finished, you'll have two empty folders, the contents of which can only be viewed by members of the Marketing or Engineering teams. Only Marketing team members can edit the contents of the Analytics folder, only Engineering team members can edit the contents of the Application folder. + +## Define granular permissions + +By using folders and teams, you avoid having to manage permissions for individual users. + +However, there are times when you need to configure permissions on a more granular level. For these cases, Grafana allows you to override permissions for specific dashboards. + +### Exercise + +Graphona has hired a consultant to assist the Marketing team. The consultant should only be able to access the SEO dashboard in the Analytics folder. + +| Name | Email | Username | +| ---------- | -------------------------------- | ---------- | +| Luc Masson | luc.masson@exampleconsulting.com | luc.masson | + +#### Add a new user + +1. In the sidebar, click the **Server Admin** (shield) icon. +1. In the Users tab, click **New user**. +1. In **Name**, enter the name of the user. +1. In **E-mail**, enter the email of the user. +1. In **Username**, enter the username that the user will use to log in. +1. In **Password**, enter a password. The user can change their password once they log in. +1. Click **Create user** to create the user account. + +#### Create a dashboard + +1. In the sidebar, click the **Create** (plus) icon to create a new dashboard. +1. In the top right corner, click the cog icon to go to **Dashboard settings**. +1. In **Name**, enter **SEO**. +1. Click **Save Dashboard**. +1. In the **Save dashboard as...** pop-up, choose the **Analytics** folder from the drop-down and click **Save**. + +#### Grant a user permission to view dashboard + +1. In the top right corner of your dashboard, click the cog icon to go to **Dashboard settings**. +1. Go to the **Permissions** tab, and click **Add Permission**. +1. In the **Add Permission For** dialog, select **User** in the first box. +1. In the second box, select the user to grant access to: Luc Masson. +1. In the third box, select **View**. +1. Click **Save**. +1. Click **Save dashboard**. +1. Add a note about giving Luc Masson Viewer permission for the dashboard and then click **Save**. + +You've created a new user and given them unique permissions to view a single dashboard within a folder. + +#### Check your work + +You can repeat these steps to log in as the other users you've created see the differences in the viewer and editor roles. + +For this example, you can log in as the user `luc.masson` to see that they can only access the SEO dashboard. + +1. Click the profile (avatar) button in the bottom left corner, choose **Sign out**. +1. Enter `luc.masson` as the username. +1. Enter the password you created for Luc. +1. Click **Log in**. +1. In the sidebar, hover your cursor over the **Dashboards** (four squares) icon and then click **Browse**. +1. You'll notice that you won't see the **Analytics** folder in the folder view because we did not give Luc folder permission. +1. Click on the list icon (3 lines) to see the dashboard list. +1. Click on the **SEO dashboard**, there shouldn't be any editing permissions since we assigned Luc the viewer role. + +## Summary + +In this tutorial, you've configured Grafana for an organization: + +- You added users to your organization. +- You created teams to manage permissions for groups of users. +- You configured permissions for folders and dashboard. + +### Learn more + +- [Organization Roles](/docs/grafana/next/administration/manage-users-and-permissions/about-users-and-permissions/#organization-roles) +- [Permissions Overview](/docs/grafana/latest/administration/manage-users-and-permissions/about-users-and-permissions/#about-users-and-permissions) diff --git a/docs/sources/tutorials/grafana-fundamentals/index.md b/docs/sources/tutorials/grafana-fundamentals/index.md new file mode 100644 index 00000000000..5374ac91ae0 --- /dev/null +++ b/docs/sources/tutorials/grafana-fundamentals/index.md @@ -0,0 +1,354 @@ +--- +title: Grafana fundamentals +summary: Get familiar with Grafana +description: Get familiar with Grafana +id: grafana-fundamentals +categories: ['fundamentals'] +tags: ['beginner'] +status: Published +authors: ['grafana_labs'] +Feedback Link: https://github.com/grafana/tutorials/issues/new +weight: 10 +--- + +## Introduction + +In this tutorial, you'll learn how to use Grafana to set up a monitoring solution for your application. + +In this tutorial, you'll: + +- Explore metrics and logs +- Build dashboards +- Annotate dashboards +- Set up alerts + +{{% class "prerequisite-section" %}} + +### Prerequisites + +- [Docker](https://docs.docker.com/install/) +- [Docker Compose](https://docs.docker.com/compose/) (included in Docker for Desktop for macOS and Windows) +- [Git](https://git-scm.com/) + {{% /class %}} + +## Set up the sample application + +This tutorial uses a sample application to demonstrate some of the features in Grafana. To complete the exercises in this tutorial, you need to download the files to your local machine. + +In this step, you'll set up the sample application, as well as supporting services, such as [Prometheus](https://prometheus.io/) and [Loki](/oss/loki/). + +1. Clone the [github.com/grafana/tutorial-environment](https://github.com/grafana/tutorial-environment) repository. + + ``` + git clone https://github.com/grafana/tutorial-environment.git + ``` + +1. Change to the directory where you cloned this repository: + + ``` + cd tutorial-environment + ``` + +1. Make sure Docker is running: + + ``` + docker ps + ``` + + No errors means it is running. If you get an error, then start Docker and then run the command again. + +1. Start the sample application: + + ``` + docker-compose up -d + ``` + + The first time you run `docker-compose up -d`, Docker downloads all the necessary resources for the tutorial. This might take a few minutes, depending on your internet connection. + + > **Note:** If you already have Grafana, Loki, or Prometheus running on your system, then you might see errors because the Docker image is trying to use ports that your local installations are already using. Stop the services, then run the command again. + +1. Ensure all services are up-and-running: + + ``` + docker-compose ps + ``` + + In the **State** column, it should say `Up` for all services. + +1. Browse to the sample application on [localhost:8081](http://localhost:8081). + +### Grafana News + +The sample application, Grafana News, lets you post links and vote for the ones you like. + +To add a link: + +1. In **Title**, enter **Example**. +1. In **URL**, enter **https://example.com**. +1. Click **Submit** to add the link. + + The link appears in the list under the Grafana News heading. + +To vote for a link, click the triangle icon next to the name of the link. + +## Log in to Grafana + +Grafana is an open-source platform for monitoring and observability that lets you visualize and explore the state of your systems. + +1. Open a new tab. +1. Browse to [localhost:3000](http://localhost:3000). +1. In **email or username**, enter **admin**. +1. In **password**, enter **admin**. +1. Click **Log In**. + + The first time you log in, you're asked to change your password: + +1. In **New password**, enter your new password. +1. In **Confirm new password**, enter the same password. +1. Click **Save**. + +The first thing you see is the Home dashboard, which helps you get started. + +To the far left you can see the _sidebar_, a set of quick access icons for navigating Grafana. + +## Add a metrics data source + +The sample application exposes metrics which are stored in [Prometheus](https://prometheus.io/), a popular time series database (TSDB). + +To be able to visualize the metrics from Prometheus, you first need to add it as a data source in Grafana. + +1. In the sidebar, hover your cursor over the **Configuration** (gear) icon, and then click **Data sources**. +1. Click **Add data source**. +1. In the list of data sources, click **Prometheus**. +1. In the URL box, enter **http\://prometheus:9090**. +1. Click **Save & test**. + + Prometheus is now available as a data source in Grafana. + +## Explore your metrics + +Grafana Explore is a workflow for troubleshooting and data exploration. In this step, you'll be using Explore to create ad-hoc queries to understand the metrics exposed by the sample application. + +> Ad-hoc queries are queries that are made interactively, with the purpose of exploring data. An ad-hoc query is commonly followed by another, more specific query. + +1. In the sidebar, click the **Explore** (compass) icon. +1. In the **Query editor**, where it says _Enter a PromQL query…_, enter `tns_request_duration_seconds_count` and then press Shift + Enter. + A graph appears. +1. In the top right corner, click the dropdown arrow on the **Run Query** button, and then select **5s**. Grafana runs your query and updates the graph every 5 seconds. + + You just made your first _PromQL_ query! [PromQL](https://prometheus.io/docs/prometheus/latest/querying/basics/) is a powerful query language that lets you select and aggregate time series data stored in Prometheus. + + `tns_request_duration_seconds_count` is a _counter_, a type of metric whose value only ever increases. Rather than visualizing the actual value, you can use counters to calculate the _rate of change_, i.e. how fast the value increases. + +1. Add the [`rate`](https://prometheus.io/docs/prometheus/latest/querying/functions/#rate) function to your query to visualize the rate of requests per second. Enter the following in the **Query editor** and then press Shift + Enter. + + ``` + rate(tns_request_duration_seconds_count[5m]) + ``` + + Immediately below the graph there's an area where each time series is listed with a colored icon next to it. This area is called the _legend_. + + PromQL lets you group the time series by their labels, using the [`sum`](https://prometheus.io/docs/prometheus/latest/querying/operators/#aggregation-operators) aggregation operator. + +1. Add the `sum` aggregation operator to your query to group time series by route: + + ``` + sum(rate(tns_request_duration_seconds_count[5m])) by(route) + ``` + +1. Go back to the [sample application](http://localhost:8081) and generate some traffic by adding new links, voting, or just refresh the browser. + +1. In the upper-right corner, click the _time picker_, and select **Last 5 minutes**. By zooming in on the last few minutes, it's easier to see when you receive new data. + +Depending on your use case, you might want to group on other labels. Try grouping by other labels, such as `status_code`, by changing the `by(route)` part of the query. + +## Add a logging data source + +Grafana supports log data sources, like [Loki](/oss/loki/). Just like for metrics, you first need to add your data source to Grafana. + +1. In the sidebar, hover your cursor over the **Configuration** (gear) icon, and then click **Data Sources**. +1. Click **Add data source**. +1. In the list of data sources, click **Loki**. +1. In the URL box, enter [http://loki:3100](http://loki:3100). +1. Click **Save & Test** to save your changes. + +Loki is now available as a data source in Grafana. + +## Explore your logs + +Grafana Explore not only lets you make ad-hoc queries for metrics, but lets you explore your logs as well. + +1. In the sidebar, click the **Explore** (compass) icon. +1. In the data source list at the top, select the **Loki** data source. +1. In the **Query editor**, enter: + + ``` + {filename="/var/log/tns-app.log"} + ``` + +1. Grafana displays all logs within the log file of the sample application. The height of each bar in the graph encodes the number of logs that were generated at that time. + +1. Click and drag across the bars in the graph to filter logs based on time. + +Not only does Loki let you filter logs based on labels, but on specific occurrences. + +Let's generate an error, and analyze it with Explore. + +1. In the [sample application](http://localhost:8081), post a new link without a URL to generate an error in your browser that says `empty url`. +1. Go back to Grafana and enter the following query to filter log lines based on a substring: + + ``` + {filename="/var/log/tns-app.log"} |= "error" + ``` + +1. Click on the log line that says `level=error msg="empty url"` to see more information about the error. + + > **Note:** If you're in Live mode, clicking logs will not show more information about the error. Instead, stop and exit the live stream, then click the log line there. + +Logs are helpful for understanding what went wrong. Later in this tutorial, you'll see how you can correlate logs with metrics from Prometheus to better understand the context of the error. + +## Build a dashboard + +A _dashboard_ gives you an at-a-glance view of your data and lets you track metrics through different visualizations. + +Dashboards consist of _panels_, each representing a part of the story you want your dashboard to tell. + +Every panel consists of a _query_ and a _visualization_. The query defines _what_ data you want to display, whereas the visualization defines _how_ the data is displayed. + +1. In the sidebar, hover your cursor over the **Create** (plus sign) icon and then click **Dashboard**. +1. Click **Add a new panel**. +1. In the **Query editor** below the graph, enter the query from earlier and then press Shift + Enter: + + ``` + sum(rate(tns_request_duration_seconds_count[5m])) by(route) + ``` + +1. In the **Legend** field, enter **{{route}}** to rename the time series in the legend. The graph legend updates when you click outside the field. +1. In the Panel editor on the right, under **Settings**, change the panel title to "Traffic". +1. Click **Apply** in the top-right corner to save the panel and go back to the dashboard view. +1. Click the **Save dashboard** (disk) icon at the top of the dashboard to save your dashboard. +1. Enter a name in the **Dashboard name** field and then click **Save**. + +## Annotate events + +When things go bad, it often helps if you understand the context in which the failure occurred. Time of last deploy, system changes, or database migration can offer insight into what might have caused an outage. Annotations allow you to represent such events directly on your graphs. + +In the next part of the tutorial, we will simulate some common use cases that someone would add annotations for. + +1. To manually add an annotation, click anywhere in your graph, then click **Add annotation**. +1. In **Description**, enter **Migrated user database**. +1. Click **Save**. + + Grafana adds your annotation to the graph. Hover your mouse over the base of the annotation to read the text. + +Grafana also lets you annotate a time interval, with _region annotations_. + +Add a region annotation: + +1. Press Ctrl (or Cmd on macOS), then click and drag across the graph to select an area. +1. In **Description**, enter **Performed load tests**. +1. In **Tags**, enter **testing**. + +Manually annotating your dashboard is fine for those single events. For regularly occurring events, such as deploying a new release, Grafana supports querying annotations from one of your data sources. Let's create an annotation using the Loki data source we added earlier. + +1. At the top of the dashboard, click the **Dashboard settings** (gear) icon. +1. Go to **Annotations** and click **Add annotation query**. +1. In **Name**, enter **Errors**. +1. In **Data source**, select **Loki**. +1. In **Query**, enter the following query: + + ``` + {filename="/var/log/tns-app.log"} |= "error" + ``` + + + +1. Click **Add**. Grafana displays the Annotations list, with your new annotation. +1. Click the **Go back** arrow to return to your dashboard. +1. At the top of your dashboard, there is now a toggle to display the results of the newly created annotation query. Press it so that it's enabled. + +The log lines returned by your query are now displayed as annotations in the graph. + +Being able to combine data from multiple data sources in one graph allows you to correlate information from both Prometheus and Loki. + +Annotations also work very well alongside alerts. In the next and final section, we will set up an alert for our app `grafana.news` and then we will trigger it. This will provide a quick intro to our new Alerting platform. + +## Create a Grafana Managed Alert + +Alerts allow you to identify problems in your system moments after they occur. By quickly identifying unintended changes in your system, you can minimize disruptions to your services. + +Grafana's new alerting platform debuted with Grafana 8. A year later, with Grafana 9, it became the default alerting method. In this step we will create a Grafana Managed Alert. Then we will trigger our new alert and send a test message to a dummy endpoint. + +The most basic alert consists of two parts: + +1. A _Contact Point_ - A Contact point defines how Grafana delivers an alert. When the conditions of an _alert rule_ are met, Grafana notifies the contact points, or channels, configured for that alert. Some popular channels include email, webhooks, Slack notifications, and PagerDuty notifications. +1. An _Alert rule_ - An Alert rule defines one or more _conditions_ that Grafana regularly evaluates. When these evaluations meet the rule's criteria, the alert is triggered. + +To begin, let's set up a webhook Contact Point. Once we have a usable endpoint, we'll write an alert rule and trigger a notification. + +### Create a Contact Point for Grafana Managed Alerts + +In this step, we'll set up a new Contact Point. This contact point will use the _webhooks_ channel. In order to make this work, we also need an endpoint for our webhook channel to receive the alert. We will use [requestbin.com](https://requestbin.com) to quickly set up that test endpoint. This way we can make sure that our alert is actually sending a notification somewhere. + +1. Browse to [requestbin.com](https://requestbin.com). +1. Under the **Create Request Bin** button, click the **public bin** link. + +Your request bin is now waiting for the first request. + +1. Copy the endpoint URL. + +Next, let's configure a Contact Point in Grafana's Alerting UI to send notifications to our Request Bin. + +1. Return to Grafana. In Grafana's sidebar, hover your cursor over the **Alerting** (bell) icon and then click **Contact points**. +1. Click **+ New contact point**. +1. In **Name**, write **RequestBin**. +1. In **Contact point type**, choose **Webhook**. +1. In **Url**, paste the endpoint to your request bin. +1. Click **Test** to send a test alert to your request bin. +1. Navigate back to the request bin you created earlier. On the left side, there's now a `POST /` entry. Click it to see what information Grafana sent. +1. Return to Grafana and click **Save contact point**. + +We have now created a dummy webhook endpoint and created a new Alerting Contact Point in Grafana. Now we can create an alert rule and link it to this new channel. + +### Add an Alert Rule to Grafana + +Now that Grafana knows how to notify us, it's time to set up an alert rule: + +1. In Grafana's sidebar, hover the cursor over the **Alerting** (bell) icon and then click **Alert rules**. +1. Click **+ New alert rule**. +1. For **Section 1**, name the rule `fundamentals-test`, and set **Rule type** to **Grafana Managed Alert**. For **Folder** type `fundamentals` and in the box that appears, press **Create: fundamentals**. +1. For **Section 2**, find the **query A** box. Choose your Prometheus datasource and enter the same query that we used in our earlier panel: `sum(rate(tns_request_duration_seconds_count[5m])) by(route)`. Press **Run queries**. You should see some data in the graph. +1. Now scroll down to the **query B** box. For **Operation** choose `Classic condition`. [You can read more about classic and multi-dimensional conditions here](/docs/grafana/latest/alerting/unified-alerting/alerting-rules/create-grafana-managed-rule/#single-and-multi-dimensional-rule). For conditions enter the following: `WHEN last() OF A IS ABOVE 0.2` +1. In **Section 3**, enter `30s` for the **Evaluate every** field. For the purposes of this tutorial, the evaluation interval is intentionally short. This makes it easier to test. In the **for** field, enter `0m`. This setting makes Grafana wait until an alert has fired for a given time before Grafana sends the notification. +1. In **Section 4**, you can add some sample text to your summary message. [Read more about message templating here](/docs/grafana/latest/alerting/unified-alerting/message-templating/). +1. Click **Save and exit** at the top of the page. +1. In Grafana's sidebar, hover the cursor over the **Alerting** (bell) icon and then click **Notification policies**. +1. Under **Root policy**, press **Edit** and change the **Default contact point** to **RequestBin**. As a system grows, admins can use the **Notification policies** setting to organize and match alert rules to specific contact points. + +### Trigger a Grafana Managed Alert + +We have now configured an alert rule and a contact point. Now let's see if we can trigger a Grafana Managed Alert by generating some traffic on our sample application. + +1. Browse to [localhost:8081](http://localhost:8081). +1. Repeatedly click the vote button or refresh the page to generate a traffic spike. + +Once the query `sum(rate(tns_request_duration_seconds_count[5m])) by(route)` returns a value greater than `0.2` Grafana will trigger our alert. Browse to the Request Bin we created earlier and find the sent Grafana alert notification with details and metadata. + +## Summary + +In this tutorial you learned about fundamental features of Grafana. To do so, we ran several Docker containers on your local machine. When you are ready to clean up this local tutorial environment, run the following command: + +``` +docker-compose down -v +``` + +### Learn more + +Check out the links below to continue your learning journey with Grafana's LGTM stack. + +- [Prometheus](/docs/grafana/latest/features/datasources/prometheus/) +- [Loki](/docs/grafana/latest/features/datasources/loki/) +- [Explore](/docs/grafana/latest/features/explore/) +- [Alerting Overview](/docs/grafana/latest/alerting/) +- [Alert rules](/docs/grafana/latest/alerting/create-alerts/) +- [Contact Points](/docs/grafana/latest/alerting/notifications/) diff --git a/docs/sources/tutorials/iis/index.md b/docs/sources/tutorials/iis/index.md new file mode 100644 index 00000000000..ec4a190a128 --- /dev/null +++ b/docs/sources/tutorials/iis/index.md @@ -0,0 +1,146 @@ +--- +title: Use IIS with URL Rewrite as a reverse proxy +summary: Learn how to set up Grafana behind IIS with URL Rewrite. +description: Learn how to set up Grafana behind IIS with URL Rewrite. +id: iis +categories: ['administration'] +tags: ['advanced'] +status: Published +authors: ['grafana_labs'] +Feedback Link: https://github.com/grafana/tutorials/issues/new +aliases: ['/docs/grafana/latest/tutorials/iis/'] +--- + +# Use IIS with URL Rewrite as a reverse proxy + +If you want Grafana to be a subpath/subfolder under a website in IIS then the Application Request Routing (ARR) and URL Rewrite modules for ISS can be used to support this. + +Example: + +- Parent site: http://yourdomain.com:8080 +- Grafana: http://localhost:3000 + +Grafana as a subpath: http://yourdomain.com:8080/grafana + +Other Examples: + +- If the application is only served on the local server, the parent site can also look like http://localhost:8080. +- If your domain is served using https on port 443, and thus the port is not normally entered in the address of your site, then the need to specify a port for the parent site in the configuration steps below can be eliminated. + +## Setup + +Install the URL Rewrite module for IIS. + +- Download and install the URL Rewrite module for IIS: https://www.iis.net/downloads/microsoft/url-rewrite + +You will also need the Application Request Routing (ARR) module for IIS for proxy forwarding + +- Download and install ARR module for IIS: https://www.iis.net/downloads/microsoft/application-request-routing + +## Grafana Config + +The Grafana config can be set by creating a file named/editing the existing file named `custom.ini` in the `conf` subdirectory of your Grafana installation. See the [installation instructions](http://docs.grafana.org/installation/windows/#configure) for more details. + +Using the example from above, if the subpath is `grafana` (you can set this to whatever is required) and the parent site is `yourdomain.com:8080`, then you would add this to the `custom.ini` config file: + +```bash +[server] +domain = yourdomain.com:8080 +root_url = %(protocol)s://%(domain)s/grafana/ +``` + +Restart the Grafana server after changing the config file. + +Configured address to serve Grafana: http://yourdomain.com:8080/grafana + +--- + +If you already have a subpath on your domain, configure it as follows: + +- Your Parent Site Address: http://yourdomain.com/existingsubpath + +```bash +[server] +domain = yourdomain.com/existingsubpath +root_url = %(protocol)s://%(domain)s/grafana/ +``` + +Restart the Grafana server after changing the config file. + +Configured address to serve Grafana: http://yourdomain.com/existingsubpath/grafana + +## IIS Config + +### Step 1: Forward Proxy + +1. Open the IIS Manager and click on the server +2. In the admin console for the server, double click on the Application Request Routing option: +3. Click the `Server Proxy Settings` action on the right-hand pane +4. Select the `Enable proxy` checkbox so that it is enabled +5. Click `Apply` and proceed with the URL Rewriting configuration + +**Note:** If you don't enable the Forward Proxy, you will most likely get 404 Not Found if you only apply the URL Rewrite rule + +### Step 2: URL Rewriting + +1. In the IIS Manager, click on the website that grafana will run under. For example, select the website that is bound to the http://yourdomain.com domain. +2. In the admin console for this website, double click on the URL Rewrite option: + +{{< figure src="/static/img/docs/tutorials/IIS_admin_console.png" max-width="800px" >}} + +3. Click on the `Add Rule(s)...` action +4. Choose the Blank Rule template for an Inbound Rule + +{{< figure src="/static/img/docs/tutorials/IIS_add_inbound_rule.png" max-width="800px" >}} + +5. Create an Inbound Rule for the website with the following settings: + +- pattern: `grafana(/)?(.*)` (if you have customised the subpath that will be used, use that instead of `grafana`) +- check the `Ignore case` checkbox +- rewrite URL set to `http://localhost:3000/{R:2}` +- check the `Append query string` checkbox +- check the `Stop processing of subsequent rules` checkbox + +{{< figure src="/static/img/docs/tutorials/IIS_url_rewrite.png" max-width="800px" >}} + +6. If your version of Grafana is greater than 8.3.5, you also need to configure the reverse proxy to preserve host headers. + +- This can be achieved by configuring the IIS config file by running this in a cmd prompt + `%windir%\system32\inetsrv\appcmd.exe set config -section:system.webServer/proxy -preserveHostHeader:true /commit:apphost` +- More information here https://github.com/grafana/grafana/issues/45261 + +Finally, navigate to `http://yourdomain.com:8080/grafana` and you should come to the Grafana login page. + +## Troubleshooting + +### 404 error + +When navigating to the Grafana URL (`http://yourdomain.com:8080/grafana`) and a `HTTP Error 404.0 - Not Found` error is returned, then either: + +- The pattern for the Inbound Rule is incorrect. Edit the rule, click on the `Test pattern...` button, test the part of the URL after `http://yourdomain.com:8080/` and make sure it matches. For `grafana/login` the test should return 3 capture groups: {R:0}: `grafana` {R:1}: `/` and {R:2}: `login`. +- The `root_url` setting in the Grafana config file does not match the parent URL with subpath. + +### Grafana Website only shows text with no images or css + +{{< figure src="/static/img/docs/tutorials/IIS_proxy_error.png" max-width="800px" >}} + +1. The `root_url` setting in the Grafana config file does not match the parent URL with subpath. This could happen if the root_url is commented out by mistake (`;` is used for commenting out a line in .ini files): + + `; root_url = %(protocol)s://%(domain)s/grafana/` + +2. or if the subpath in the `root_url` setting does not match the subpath used in the pattern in the Inbound Rule in IIS: + + `root_url = %(protocol)s://%(domain)s/grafana/` + + pattern in Inbound Rule: `wrongsubpath(/)?(.*)` + +3. or if the Rewrite URL in the Inbound Rule is incorrect. + + The Rewrite URL should not include the subpath. + + The Rewrite URL should contain the capture group from the pattern matching that returns the part of the URL after the subpath. The pattern used above returns three capture groups and the third one {R:2} returns the part of the URL after `http://yourdomain.com:8080/grafana/`. + +### You see an 'Error updating options: origin not allowed' error + +- Ensure you have undertaken step 6 above, to configure IIS to preserve host headers by edit IIS config by running this in cmd prompt: + `%windir%\system32\inetsrv\appcmd.exe set config -section:system.webServer/proxy -preserveHostHeader:true /commit:apphost` diff --git a/docs/sources/tutorials/install-grafana-on-raspberry-pi/index.md b/docs/sources/tutorials/install-grafana-on-raspberry-pi/index.md new file mode 100644 index 00000000000..16870ac0f92 --- /dev/null +++ b/docs/sources/tutorials/install-grafana-on-raspberry-pi/index.md @@ -0,0 +1,147 @@ +--- +title: Install Grafana on Raspberry Pi +summary: Get Grafana set up on your Raspberry Pi. +description: Get Grafana set up on your Raspberry Pi. +id: install-grafana-on-raspberry-pi +categories: ['administration'] +tags: ['beginner'] +authors: ['grafana_labs'] +Feedback Link: https://github.com/grafana/tutorials/issues/new +--- + +## Introduction + +The Raspberry Pi is a tiny, affordable, yet capable computer that can run a range of different applications. Even Grafana! + +Many people are running Grafana on Raspberry Pi as a way to monitor their home, for things like indoor temperature, humidity, or energy usage. + +In this tutorial, you'll: + +- Set up a Raspberry Pi using a version of Raspberry Pi OS (previously called "Raspbian") that does not require you to connect a keyboard or monitor (this is often called "headless"). +- Install Grafana on your Raspberry Pi. + +{{% class "prerequisite-section" %}} + +### Prerequisites + +- Raspberry Pi +- SD card + {{% /class %}} + +## Set up your Raspberry Pi + +Before we can install Grafana, you first need to set up your Raspberry Pi. + +For this tutorial, you'll configure your Raspberry Pi to be _headless_. This means you don't need to connect a monitor, keyboard, or mouse to your Raspberry Pi. All configuration is done from your regular computer. + +#### Download and install Raspberry Pi Imager + +Before we get started, you need to download and install the [Raspberry Pi Imager](https://www.raspberrypi.org/software/). + +We'll use the Raspberry Pi Imager to flash the operating system image to the SD card. You download the imager directly from the official Raspberry Pi website and it's available for Ubuntu Linux, macOS, and Windows. + +Follow the directions on the website to download and install the imager. + +#### Install Raspberry Pi OS + +Now it is time to install Raspberry Pi OS. + +1. Insert the SD card into your regular computer from which you plan to install Raspberry Pi OS. +1. Run the Raspberry Pi Imager that you downloaded and installed. +1. To select an operating system, click **Choose OS** in the imager. You will be shown a list of available options. +1. From the list, select **Raspberry Pi OS (other)** and then select **Raspberry Pi OS Lite**, which is a Debian-based operating system for the Raspberry Pi. Since you're going to run a headless Raspberry Pi, you won't need the desktop dependencies. +1. To select where you want to put the operating system image, click **Choose Storage** in the imager and then select the SD card you already inserted into your computer. +1. The final step in the imager to click **Write**. When you do, the imager will write the Raspberry Pi OS Lite image to the SD card and verify that it has been written correctly. +1. Eject the SD card from your computer, and insert it again. + +While you _could_ fire up the Raspberry Pi now, we don't yet have any way of accessing it. + +1. Create an empty file called `ssh` in the boot directory. This enables SSH so that you can log in remotely. + + The next step is only required if you want the Raspberry Pi to connect to your wireless network. Otherwise, connect the it to your network by using a network cable. + +1. **(Optional)** Create a file called `wpa_supplicant.conf` in the boot directory: + + ``` + ctrl_interface=/var/run/wpa_supplicant + update_config=1 + country= + + network={ + ssid="" + psk="" + } + ``` + +All the necessary files are now on the SD card. Let's start up the Raspberry Pi. + +1. Eject the SD card and insert it into the SD card slot on the Raspberry Pi. +1. Connect the power cable and make sure the LED lights are on. +1. Find the IP address of the Raspberry Pi. Usually you can find the address in the control panel for your WiFi router. + +#### Connect remotely via SSH + +1. Open up your terminal and enter the following command: + ``` + ssh pi@ + ``` +1. SSH warns you that the authenticity of the host can't be established. Type "yes" to continue connecting. +1. When asked for a password, enter the default password: `raspberry`. +1. Once you're logged in, change the default password: + ``` + passwd + ``` + +Congratulations! You've now got a tiny Linux machine running that you can hide in a closet and access from your normal workstation. + +## Install Grafana + +Now that you've got the Raspberry Pi up and running, the next step is to install Grafana. + +1. Add the APT key used to authenticate packages: + + ``` + wget -q -O - https://packages.grafana.com/gpg.key | sudo apt-key add - + ``` + +1. Add the Grafana APT repository: + + ``` + echo "deb https://packages.grafana.com/oss/deb stable main" | sudo tee -a /etc/apt/sources.list.d/grafana.list + ``` + +1. Install Grafana: + ``` + sudo apt-get update + sudo apt-get install -y grafana + ``` + +Grafana is now installed, but not yet running. To make sure Grafana starts up even if the Raspberry Pi is restarted, we need to enable and start the Grafana Systemctl service. + +1. Enable the Grafana server: + + ``` + sudo /bin/systemctl enable grafana-server + ``` + +1. Start the Grafana server: + + ``` + sudo /bin/systemctl start grafana-server + ``` + + Grafana is now running on the machine and is accessible from any device on the local network. + +1. Open a browser and go to `http://:3000`, where the IP address is the address that you used to connect to the Raspberry Pi earlier. You're greeted with the Grafana login page. +1. Log in to Grafana with the default username `admin`, and the default password `admin`. +1. Change the password for the admin user when asked. + +Congratulations! Grafana is now running on your Raspberry Pi. If the Raspberry Pi is ever restarted or turned off, Grafana will start up whenever the machine regains power. + +## Summary + +If you want to use Grafana without having to go through a full installation process, check out [Grafana Cloud](/products/cloud/), which is designed to get users up and running quickly and easily. Grafana Cloud offers a forever free plan that is genuinely useful for hobbyists, testing, and small teams. + +### Learn more + +- [Raspberry Pi Documentation](https://www.raspberrypi.org/documentation/) diff --git a/docs/sources/tutorials/integrate-hubot/index.md b/docs/sources/tutorials/integrate-hubot/index.md new file mode 100644 index 00000000000..0d0af2f3821 --- /dev/null +++ b/docs/sources/tutorials/integrate-hubot/index.md @@ -0,0 +1,118 @@ +--- +title: Integrate Hubot with Grafana +summary: Learn how to integrate Hubot with Grafana +description: Learn how to integrate Hubot with Grafana +id: integrate-hubot +categories: ['administration'] +tags: ['advanced'] +status: Published +authors: ['grafana_labs'] +Feedback Link: https://github.com/grafana/tutorials/issues/new +aliases: ['/docs/grafana/latest/tutorials/hubot_howto/'] +--- + +# Integrate Hubot with Grafana + +Grafana 2.0 shipped with a great feature that enables it to render any graph or panel to a PNG image. + +No matter what data source you are using, the PNG image of the Graph will look the same as it does in your browser. + +This guide will show you how to install and configure the [Hubot-Grafana](https://github.com/stephenyeargin/hubot-grafana) plugin. This plugin allows you to tell hubot to render any dashboard or graph right from a channel in Slack, Hipchat or Basecamp. The bot will respond with an image of the graph and a link that will take you to the graph. + +> _Amazon S3 Required_: The hubot-grafana script will upload the rendered graphs to Amazon S3. This +> is so Hipchat and Slack can show them reliably (they require the image to be publicly available). + +{{< figure src="/static/img/docs/tutorials/hubot_grafana.png" max-width="800px" >}} + +## What is Hubot? + +[Hubot](https://hubot.github.com/) is an universal and extensible chat bot that can be used with many chat services and has a huge library of third party plugins that allow you to automate anything from your chat rooms. + +## Install Hubot + +Hubot is very easy to install and host. If you do not already have a bot up and running please read the official [Getting Started With Hubot](https://hubot.github.com/docs/) guide. + +## Install Hubot-Grafana script + +In your Hubot project repo install the Grafana plugin using `npm`: + +```bash +npm install hubot-grafana --save +``` + +Edit the file external-scripts.json, and add hubot-grafana to the list of plugins. + +```json +["hubot-pugme", "hubot-shipit", "hubot-grafana"] +``` + +## Configure + +The `hubot-grafana` plugin requires a number of environment variables to be set in order to work properly. + +```bash +export HUBOT_GRAFANA_HOST=https://play.grafana.org +export HUBOT_GRAFANA_API_KEY=abcd01234deadbeef01234 +export HUBOT_GRAFANA_S3_BUCKET=mybucket +export HUBOT_GRAFANA_S3_ACCESS_KEY_ID=ABCDEF123456XYZ +export HUBOT_GRAFANA_S3_SECRET_ACCESS_KEY=aBcD01234dEaDbEef01234 +export HUBOT_GRAFANA_S3_PREFIX=graphs +export HUBOT_GRAFANA_S3_REGION=us-standard +``` + +### Grafana server side rendering + +The hubot plugin will take advantage of the Grafana server side rendering feature that can render any panel on the server using phantomjs. Grafana ships with a phantomjs binary (Linux only). + +To verify that this feature works try the `Direct link to rendered image` link in the panel share dialog. If you do not get an image when opening this link verify that the required font packages are installed for phantomjs to work. + +### Grafana API Key + +{{< figure src="/static/img/docs/v2/orgdropdown_api_keys.png" max-width="150px" class="docs-image--right">}} + +You need to set the environment variable `HUBOT_GRAFANA_API_KEY` to a Grafana API Key. You can add these from the API Keys page which you find in the Organization dropdown. + +### Amazon S3 + +The `S3` options are optional but for the images to work properly in services like Slack and Hipchat they need to publicly available. By specifying the `S3` options the hubot-grafana script will publish the rendered panel to `S3` and it will use that URL when it posts to Slack or Hipchat. + +## Hubot commands + +- `hubot graf list` + - Lists the available dashboards +- `hubot graf db graphite-carbon-metrics` + - Graph all panels in the dashboard +- `hubot graf db graphite-carbon-metrics:3` + - Graph only panel with id 3 of a particular dashboard +- `hubot graf db graphite-carbon-metrics:cpu` + - Graph only the panels containing "cpu" (case insensitive) in the title +- `hubot graf db graphite-carbon-metrics now-12hr` + - Get a dashboard with a window of 12 hours ago to now +- `hubot graf db graphite-carbon-metrics now-24hr now-12hr` + - Get a dashboard with a window of 24 hours ago to 12 hours ago +- `hubot graf db graphite-carbon-metrics:3 now-8d now-1d` + - Get only the third panel of a particular dashboard with a window of 8 days ago to yesterday +- `hubot graf db graphite-carbon-metrics host=carbon-a` + - Get a templated dashboard with the `$host` parameter set to `carbon-a` + +## Aliases + +Some of the hubot commands above can lengthy and you might have to remember the dashboard slug (url id). If you have a few favorite graphs you want to be able check up on often (let's say from your mobile) you can create hubot command aliases with the hubot script `hubot-alias`. + +Install it: + +```bash +npm i --save hubot-alias +``` + +Now add `hubot-alias` to the list of plugins in `external-scripts.json` and restart hubot. + +Now you can add an alias like this: + +- `hubot alias graf-lb=graf db loadbalancers:2 now-20m` + +{{< figure src="/static/img/docs/tutorials/hubot_grafana2.png" max-width="800px" >}} + +## Summary + +Grafana is going to ship with integrated Slack and Hipchat features some day but you do not have to wait for that. Grafana 2 shipped with a very clever server side rendering feature that can render any panel to a png using phantomjs. The hubot plugin for Grafana is something you can install and use today! diff --git a/docs/sources/tutorials/provision-dashboards-and-data-sources/index.md b/docs/sources/tutorials/provision-dashboards-and-data-sources/index.md new file mode 100644 index 00000000000..4554b67073f --- /dev/null +++ b/docs/sources/tutorials/provision-dashboards-and-data-sources/index.md @@ -0,0 +1,260 @@ +--- +title: Provision dashboards and data sources +summary: Treat your configuration as code. +description: Treat your configuration as code. +id: provision-dashboards-and-data-sources +categories: ['administration'] +tags: ['intermediate'] +authors: ['grafana_labs'] +Feedback Link: https://github.com/grafana/tutorials/issues/new +weight: 40 +--- + +## Introduction + +Learn how you can reuse dashboards and data sources across multiple teams by provisioning Grafana from version-controlled configuration files. + +In this tutorial, you'll: + +- Provision dashboards. +- Provision data sources. + +{{% class "prerequisite-section" %}} + +### Prerequisites + +- Grafana 7.0 +- Administrator privileges on the system you are doing the tutorial on + {{% /class %}} + +## Configuration as code + +Configuration as code is the practice of storing the configuration of your system as a set of version controlled, human-readable configuration files, rather than in a database. These configuration files can be reused across environments to avoid duplicated resources. + +As the number of dashboards and data sources grows within your organization, manually managing changes can become tedious and error-prone. Encouraging reuse becomes important to avoid multiple teams redesigning the same dashboards. + +Grafana supports configuration as code through _provisioning_. The resources that currently supports provisioning are: + +- [Dashboards](/docs/grafana/latest/administration/provisioning/#dashboards) +- [Data sources](/docs/grafana/latest/administration/provisioning/#datasources) +- [Alert notification channels](/docs/grafana/latest/administration/provisioning/#alert-notification-channels) + +## Set the provisioning directory + +Before you can start provisioning resources, Grafana needs to know where to find the _provisioning directory_. The provisioning directory contains configuration files that are applied whenever Grafana starts and continuously updated while running. + +By default, Grafana looks for a provisioning directory in the configuration directory (grafana > conf) on the system where Grafana is installed. However, if you are a Grafana Administrator, then you might want to place the config files in a shared resource like a network folder, so you would need to change the path to the provisioning directory. + +You can set a different path by setting the `paths.provisioning` property in the main config file: + +```ini +[paths] +provisioning = +``` + +For more information about configuration files, refer to [Configuration](/docs/grafana/latest/installation/configuration/) in the [Grafana documentation](/docs/grafana/latest/). + +The provisioning directory assumes the following structure: + +``` +provisioning/ + datasources/ + + dashboards/ + + notifiers/ + +``` + +Next, we'll look at how to provision a data source. + +## Provision a data source + +Each data source provisioning config file contains a _manifest_ that specifies the desired state of a set of provisioned data sources. + +At startup, Grafana loads the configuration files and provisions the data sources listed in the manifests. + +Let's configure a [TestData DB](/docs/grafana/latest/features/datasources/testdata/) data source that you can use for your dashboards. + +#### Create a data source manifest + +1. In the `provisioning/datasources/` directory, create a file called `default.yaml` with the following content: + + ```yaml + apiVersion: 1 + + datasources: + - name: TestData DB + type: testdata + ``` + +1. Restart Grafana to load the new changes. +1. In the sidebar, hover the cursor over the **Configuration** (gear) icon and click **Data Sources**. The TestData DB appears in the list of data sources. + +> The configuration options can vary between different types of data sources. For more information on how to configure a specific data source, refer to [Data sources](/docs/grafana/latest/administration/provisioning/#datasources). + +## Provision a dashboard + +Each dashboard config file contains a manifest that specifies the desired state of a set of _dashboard providers_. + +A dashboard provider tells Grafana where to find the dashboard definitions and where to put them. + +Grafana regularly checks for changes to the dashboard definitions (by default every 10 seconds). + +Let's define a dashboard provider so that Grafana knows where to find the dashboards we want to provision. + +#### Define a dashboard provider + +In the `provisioning/dashboards/` directory, create a file called `default.yaml` with the following content: + +```yaml +apiVersion: 1 + +providers: + - name: Default # A uniquely identifiable name for the provider + folder: Services # The folder where to place the dashboards + type: file + options: + path: + + # Default path for Windows: C:/Program Files/GrafanaLabs/grafana/public/dashboards + # Default path for Linux is: /var/lib/grafana/dashboards +``` + +For more information on how to configure dashboard providers, refer to [Dashboards](/docs/grafana/latest/administration/provisioning/#dashboards). + +#### Create a dashboard definition + +1. In the dashboard definitions directory you specified in the dashboard provider, i.e. `options.path`, create a file called `cluster.json` with the following content: + + ```json + { + "__inputs": [], + "__requires": [], + "annotations": { + "list": [] + }, + "editable": false, + "gnetId": null, + "graphTooltip": 0, + "hideControls": false, + "id": null, + "links": [], + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "TestData DB", + "fill": 1, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 2, + "legend": { + "alignAsTable": false, + "avg": false, + "current": false, + "max": false, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "repeat": null, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "CPU Usage", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + } + ], + "refresh": "", + "rows": [], + "schemaVersion": 16, + "style": "dark", + "tags": ["kubernetes"], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], + "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] + }, + "timezone": "browser", + "title": "Cluster", + "version": 0 + } + ``` + +1. Restart Grafana to provision the new dashboard or wait 10 seconds for Grafana to automatically create the dashboard. +1. In the sidebar, hover the cursor over **Dashboards** (squares) icon, and then click **Manage**. The dashboard appears in a **Services** folder. + +> If you don't specify an `id` in the dashboard definition, then Grafana assigns one during provisioning. You can set the `id` yourself if you want to reference the dashboard from other dashboards. Be careful to not use the same `id` for multiple dashboards, as this will cause a conflict. + +## Summary + +In this tutorial you learned how you to reuse dashboards and data sources across multiple teams by provisioning Grafana from version-controlled configuration files. + +Dashboard definitions can get unwieldy as more panels and configurations are added to them. There are a number of open source tools available to make it easier to manage dashboard definitions: + +- [grafana-dash-gen](https://github.com/uber/grafana-dash-gen) (Javascript) +- [grafanalib](https://github.com/weaveworks/grafanalib) (Python) +- [grafonnet-lib](https://github.com/grafana/grafonnet-lib) (Jsonnet) +- [grafyaml](https://docs.openstack.org/infra/grafyaml/) (YAML) + +### Learn more + +- [Provisioning Grafana](/docs/grafana/latest/administration/provisioning/) diff --git a/docs/sources/tutorials/run-grafana-behind-a-proxy/index.md b/docs/sources/tutorials/run-grafana-behind-a-proxy/index.md new file mode 100644 index 00000000000..10be2dee35f --- /dev/null +++ b/docs/sources/tutorials/run-grafana-behind-a-proxy/index.md @@ -0,0 +1,222 @@ +--- +title: Run Grafana behind a reverse proxy +summary: Learn how to run Grafana behind a reverse proxy +description: Learn how to run Grafana behind a reverse proxy +id: run-grafana-behind-a-proxy +categories: ['administration'] +tags: ['advanced'] +status: Published +authors: ['grafana_labs'] +Feedback Link: https://github.com/grafana/tutorials/issues/new +aliases: ['/docs/grafana/latest/installation/behind_proxy/'] +--- + +## Introduction + +In this tutorial, you'll configure Grafana to run behind a reverse proxy. + +When running Grafana behind a proxy, you need to configure the domain name to let Grafana know how to render links and redirects correctly. + +- In the Grafana configuration file, change `server.domain` to the domain name you'll be using: + +```bash +[server] +domain = example.com +``` + +- Restart Grafana for the new changes to take effect. + +You can also serve Grafana behind a _sub path_, such as `http://example.com/grafana`. + +To serve Grafana behind a sub path: + +- Include the sub path at the end of the `root_url`. +- Set `serve_from_sub_path` to `true`. + +```bash +[server] +domain = example.com +root_url = %(protocol)s://%(domain)s:%(http_port)s/grafana/ +serve_from_sub_path = true +``` + +Next, you need to configure your reverse proxy. + +## Configure NGINX + +[NGINX](https://www.nginx.com) is a high performance load balancer, web server, and reverse proxy. + +- In your NGINX configuration file inside `http` section, add the following: + +```nginx +# this is required to proxy Grafana Live WebSocket connections. +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + +upstream grafana { + server localhost:3000; +} + +server { + listen 80; + root /usr/share/nginx/html; + index index.html index.htm; + + location / { + proxy_set_header Host $http_host; + proxy_pass http://grafana; + } + + # Proxy Grafana Live WebSocket connections. + location /api/live/ { + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Host $http_host; + proxy_pass http://grafana; + } +} +``` + +- Reload the NGINX configuration. +- Navigate to port 80 on the machine NGINX is running on. You're greeted by the Grafana login page. + +For Grafana Live which uses WebSocket connections you may have to raise Nginx [worker_connections](https://nginx.org/en/docs/ngx_core_module.html#worker_connections) option which is 512 by default – which limits the number of possible concurrent connections with Grafana Live. + +Also, be aware that the above configuration will work only when the `proxy_pass` value for `location /` is a literal string. If you are using a variable here, [read this GitHub issue](https://github.com/grafana/grafana/issues/18299). You will need to add [an appropriate NGINX rewrite rule](https://www.nginx.com/blog/creating-nginx-rewrite-rules/). + +To configure NGINX to serve Grafana under a _sub path_, update the `location` block: + +```nginx +# this is required to proxy Grafana Live WebSocket connections. +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + +upstream grafana { + server localhost:3000; +} + +server { + listen 80; + root /usr/share/nginx/www; + index index.html index.htm; + + location /grafana/ { + rewrite ^/grafana/(.*) /$1 break; + proxy_set_header Host $http_host; + proxy_pass http://grafana; + } + + # Proxy Grafana Live WebSocket connections. + location /grafana/api/live/ { + rewrite ^/grafana/(.*) /$1 break; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Host $http_host; + proxy_pass http://grafana; + } +} +``` + +## Configure HAProxy + +To configure HAProxy to serve Grafana under a _sub path_: + +```bash +frontend http-in + bind *:80 + use_backend grafana_backend if { path /grafana } or { path_beg /grafana/ } + +backend grafana_backend + # Requires haproxy >= 1.6 + http-request set-path %[path,regsub(^/grafana/?,/)] + + # Works for haproxy < 1.6 + # reqrep ^([^\ ]*\ /)grafana[/]?(.*) \1\2 + + server grafana localhost:3000 +``` + +## Configure IIS + +> IIS requires that the URL Rewrite module is installed. + +To configure IIS to serve Grafana under a _sub path_, create an Inbound Rule for the parent website in IIS Manager with the following settings: + +- pattern: `grafana(/)?(.*)` +- check the `Ignore case` checkbox +- rewrite URL set to `http://localhost:3000/{R:2}` +- check the `Append query string` checkbox +- check the `Stop processing of subsequent rules` checkbox + +This is the rewrite rule that is generated in the `web.config`: + +```xml + + + + + + + + +``` + +See the [tutorial on IIS URL Rewrites](/tutorials/iis/) for more in-depth instructions. + +## Configure Traefik + +[Traefik](https://traefik.io/traefik/) Cloud Native Reverse Proxy / Load Balancer / Edge Router + +Using the docker provider the following labels will configure the router and service for a domain or subdomain routing. + +```yaml +labels: + traefik.http.routers.grafana.rule: Host(`grafana.example.com`) + traefik.http.services.grafana.loadbalancer.server.port: 3000 +``` + +To deploy on a _sub path_ + +```yaml +labels: + traefik.http.routers.grafana.rule: Host(`example.com`) && PathPrefix(`/grafana`) + traefik.http.services.grafana.loadbalancer.server.port: 3000 +``` + +Examples using the file provider. + +```yaml +http: + routers: + grafana: + rule: Host(`grafana.example.com`) + service: grafana + services: + grafana: + loadBalancer: + servers: + - url: http://192.168.30.10:3000 +``` + +```yaml +http: + routers: + grafana: + rule: Host(`example.com`) && PathPrefix(`/grafana`) + service: grafana + services: + grafana: + loadBalancer: + servers: + - url: http://192.168.30.10:3000 +``` + +## Summary + +In this tutorial you learned how to run Grafana behind a reverse proxy. diff --git a/docs/sources/tutorials/stream-metrics-from-telegraf-to-grafana/index.md b/docs/sources/tutorials/stream-metrics-from-telegraf-to-grafana/index.md new file mode 100644 index 00000000000..822006b2fcf --- /dev/null +++ b/docs/sources/tutorials/stream-metrics-from-telegraf-to-grafana/index.md @@ -0,0 +1,101 @@ +--- +title: Stream metrics from Telegraf to Grafana +summary: Use Telegraf to stream live metrics to Grafana. +description: Use Telegraf to stream live metrics to Grafana. +id: stream-metrics-from-telegraf-to-grafana +categories: ['administration'] +tags: ['beginner'] +status: Published +authors: ['grafana_labs'] +Feedback Link: https://github.com/grafana/tutorials/issues/new +weight: 75 +--- + +## Introduction + +Grafana v8 introduced streaming capabilities – a way to push data to UI panels in near real-time. In this tutorial we show how Grafana real-time streaming capabilities can be used together with Telegraf to instantly display system measurements. + +In this tutorial, you'll: + +- Setup Telegraf and output measurements directly to Grafana time-series panel in near real-time + +{{% class "prerequisite-section" %}} + +#### Prerequisites + +- Grafana 8.0+ +- Telegraf + {{% /class %}} + +## Run Grafana and create admin token + +1. Run Grafana following [installation instructions](/docs/grafana/latest/installation/) for your operating system +1. Log in and go to Configuration -> API Keys +1. Press "Add API key" button and create a new API token with **Admin** role + +## Configure and run Telegraf + +Telegraf is a plugin-driven server agent for collecting and sending metrics and events from databases, systems, and IoT sensors. + +You can install it following [official installation instructions](https://docs.influxdata.com/telegraf/latest/introduction/installation/). + +In this tutorial we will be using Telegraf HTTP output plugin to send metrics in Influx format to Grafana. We can use a configuration like this: + +``` +[agent] + interval = "1s" + flush_interval = "1s" + +[[inputs.cpu]] + percpu = false + totalcpu = true + +[[outputs.http]] + url = "http://localhost:3000/api/live/push/custom_stream_id" + data_format = "influx" + [outputs.http.headers] + Authorization = "Bearer " +``` + +Make sure to replace `` placeholder with your actual API key created in the previous step. Save this config into `telegraf.conf` file and run Telegraf pointing to this config file. Telegraf will periodically (once in a second) report the state of total CPU usage on a host to Grafana (which is supposed to be running on `http://localhost:3000`). Of course you can replace `custom_stream_id` to something more meaningful for your use case. + +Inside Grafana Influx data is converted to Grafana data frames and then frames are published to Grafana Live channels. In this case, the channel where CPU data will be published is `stream/custom_stream_id/cpu`. The `stream` scope is constant, the `custom_stream_id` namespace is the last part of API URL set in Telegraf configuration (`http://localhost:3000/api/live/push/telegraf`) and the path is `cpu` - the name of a measurement. + +The only thing left here is to create a dashboard with streaming data. + +## Create dashboard with streaming data + +1. Create new dashboard +1. Press Add empty panel +1. Select `-- Grafana --` datasource +1. Select `Live Measurements` query type +1. Find and select `stream/custom_stream_id/cpu` measurement for Channel field +1. Save dashboard changes + +After making these steps Grafana UI should subscribe to the channel `stream/custom_stream_id/cpu` and you should see CPU data updates coming from Telegraf in near real-time. + +## Stream using WebSocket endpoint + +If you aim for a high-frequency update sending then you may want to use the WebSocket output plugin of Telegraf (introduced in Telegraf v1.19.0) instead of the HTTP output plugin we used above. Configure WebSocket output plugin like this: + +``` +[agent] + interval = "500ms" + flush_interval = "500ms" + +[[inputs.cpu]] + percpu = false + totalcpu = true + +[[outputs.websocket]] + url = "ws://localhost:3000/api/live/push/custom_stream_id" + data_format = "influx" + [outputs.websocket.headers] + Authorization = "Bearer " +``` + +WebSocket avoids running all Grafana HTTP middleware on each request from Telegraf thus reducing Grafana backend CPU usage significantly. + +## Summary + +In this tutorial you learned how to use Telegraf to stream live metrics to Grafana. diff --git a/e2e/cloud-plugins-suite/azure-monitor.spec.ts b/e2e/cloud-plugins-suite/azure-monitor.spec.ts index b965fa5880b..84fb21421ae 100644 --- a/e2e/cloud-plugins-suite/azure-monitor.spec.ts +++ b/e2e/cloud-plugins-suite/azure-monitor.spec.ts @@ -56,7 +56,7 @@ const addAzureMonitorVariable = ( name: string, type: AzureQueryType, isFirst: boolean, - options?: { subscription?: string; resourceGroup?: string; namespace?: string; resource?: string } + options?: { subscription?: string; resourceGroup?: string; namespace?: string; resource?: string; region?: string } ) => { e2e.components.PageToolbar.item('Dashboard settings').click(); e2e.components.Tab.title('Variables').click(); @@ -75,6 +75,9 @@ const addAzureMonitorVariable = ( case AzureQueryType.ResourceGroupsQuery: e2eSelectors.variableEditor.subscription.input().find('input').type(`${options?.subscription}{enter}`); break; + case AzureQueryType.LocationsQuery: + e2eSelectors.variableEditor.subscription.input().find('input').type(`${options?.subscription}{enter}`); + break; case AzureQueryType.NamespacesQuery: e2eSelectors.variableEditor.subscription.input().find('input').type(`${options?.subscription}{enter}`); e2eSelectors.variableEditor.resourceGroup.input().find('input').type(`${options?.resourceGroup}{enter}`); @@ -83,6 +86,7 @@ const addAzureMonitorVariable = ( e2eSelectors.variableEditor.subscription.input().find('input').type(`${options?.subscription}{enter}`); e2eSelectors.variableEditor.resourceGroup.input().find('input').type(`${options?.resourceGroup}{enter}`); e2eSelectors.variableEditor.namespace.input().find('input').type(`${options?.namespace}{enter}`); + e2eSelectors.variableEditor.region.input().find('input').type(`${options?.region}{enter}`); break; case AzureQueryType.MetricNamesQuery: e2eSelectors.variableEditor.subscription.input().find('input').type(`${options?.subscription}{enter}`); @@ -223,10 +227,14 @@ e2e.scenario({ subscription: '$subscription', resourceGroup: '$resourceGroups', }); + addAzureMonitorVariable('region', AzureQueryType.LocationsQuery, false, { + subscription: '$subscription', + }); addAzureMonitorVariable('resource', AzureQueryType.ResourceNamesQuery, false, { subscription: '$subscription', resourceGroup: '$resourceGroups', namespace: '$namespace', + region: '$region', }); e2e.pages.Dashboard.SubMenu.submenuItemLabels('subscription').click(); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('grafanalabs-datasources-dev').click(); @@ -240,6 +248,8 @@ e2e.scenario({ .parent() .find('input') .type('microsoft.storage/storageaccounts{downArrow}{enter}'); + e2e.pages.Dashboard.SubMenu.submenuItemLabels('region').parent().find('button').click(); + e2e.pages.Dashboard.SubMenu.submenuItemLabels('region').parent().find('input').type('uk south{downArrow}{enter}'); e2e.pages.Dashboard.SubMenu.submenuItemLabels('resource').parent().find('button').click(); e2e.pages.Dashboard.SubMenu.submenuItemLabels('resource') .parent() @@ -254,6 +264,7 @@ e2e.scenario({ e2eSelectors.queryEditor.resourcePicker.advanced.subscription.input().find('input').type('$subscription'); e2eSelectors.queryEditor.resourcePicker.advanced.resourceGroup.input().find('input').type('$resourceGroups'); e2eSelectors.queryEditor.resourcePicker.advanced.namespace.input().find('input').type('$namespaces'); + e2eSelectors.queryEditor.resourcePicker.advanced.region.input().find('input').type('$region'); e2eSelectors.queryEditor.resourcePicker.advanced.resource.input().find('input').type('$resource'); e2eSelectors.queryEditor.resourcePicker.apply.button().click(); e2eSelectors.queryEditor.metricsQueryEditor.metricName.input().find('input').type('Transactions{enter}'); diff --git a/e2e/run-suite b/e2e/run-suite index d74a41d67f4..8c446f3145b 100755 --- a/e2e/run-suite +++ b/e2e/run-suite @@ -18,6 +18,7 @@ args=("$@") CMD="start" PARAMS="" +CLEANUP="" declare -A env=( [BASE_URL]=${BASE_URL:-"http://$HOST:$PORT"} @@ -25,6 +26,7 @@ declare -A env=( ) testFilesForSingleSuite="*.spec.ts" +rootForEnterpriseSuite="extensions-suite" declare -A cypressConfig=( [integrationFolder]=../../e2e @@ -64,6 +66,27 @@ case "$1" in cypressConfig[screenshotsFolder]=../../e2e/benchmarks/"${args[1]}"/screenshots cypressConfig[testFiles]=$testFilesForSingleSuite ;; + "enterprise") + echo "Enterprise" + CLEANUP="rm -rf ../../e2e/extensions-suite" + SETUP="cp -Lr ../../e2e/extensions ../../e2e/extensions-suite" + enterpriseSuite=$(basename "${args[1]}") + case "$2" in + "debug") + echo -e "Debug mode" + env[SLOWMO]=1 + PARAMS="--no-exit" + enterpriseSuite=$(basename "${args[2]}") + ;; + "dev") + echo "Dev mode" + CMD="open" + enterpriseSuite=$(basename "${args[2]}") + ;; + esac + cypressConfig[testFiles]=$rootForEnterpriseSuite/$enterpriseSuite/*-suite/*.spec.ts + $CLEANUP && $SETUP + ;; "") ;; *) @@ -96,3 +119,5 @@ function join () { yarn $CMD --env "$(join env)" \ --config "$(join cypressConfig)" \ $PARAMS + +$CLEANUP diff --git a/e2e/start-and-run-suite b/e2e/start-and-run-suite index 2c5c7d1d996..d29b7775b96 100755 --- a/e2e/start-and-run-suite +++ b/e2e/start-and-run-suite @@ -2,11 +2,21 @@ . scripts/grafana-server/variables +LICENSE_PATH="" + +if [ "$1" = "enterprise" ]; then + if [ "$2" != "dev" ] && [ "$2" != "debug" ]; then + LICENSE_PATH=$2/license.jwt + else + LICENSE_PATH=$3/license.jwt + fi +fi + if [ "$BASE_URL" != "" ]; then echo -e "BASE_URL set, skipping starting server" else # Start it in the background - ./scripts/grafana-server/start-server 2>&1 > scripts/grafana-server/server.log & + ./scripts/grafana-server/start-server $LICENSE_PATH 2>&1 > scripts/grafana-server/server.log & ./scripts/grafana-server/wait-for-grafana fi diff --git a/kinds/dashboard/dashboard_kind.cue b/kinds/dashboard/dashboard_kind.cue index 8498d08b175..002331eb31b 100644 --- a/kinds/dashboard/dashboard_kind.cue +++ b/kinds/dashboard/dashboard_kind.cue @@ -30,8 +30,8 @@ lineage: seqs: [ tags?: [...string] @grafanamaturity(NeedsExpertReview) // Theme of dashboard. style: "light" | *"dark" @grafanamaturity(NeedsExpertReview) - // Timezone of dashboard, - timezone?: *"browser" | "utc" | "" @grafanamaturity(NeedsExpertReview) + // Timezone of dashboard. Accepts IANA TZDB zone ID or "browser" or "utc". + timezone?: string | *"browser" // Whether a dashboard is editable or not. editable: bool | *true // Configuration of dashboard cursor sync behavior. @@ -397,6 +397,9 @@ lineage: seqs: [ // TODO tighter constraint timeShift?: string @grafanamaturity(NeedsExpertReview) + // Dynamically load the panel + libraryPanel?: #LibraryPanelRef + // options is specified by the PanelOptions field in panel // plugin schemas. options: {...} @grafanamaturity(NeedsExpertReview) @@ -412,6 +415,11 @@ lineage: seqs: [ }] @grafanamaturity(NeedsExpertReview) } @cuetsy(kind="interface") @grafana(TSVeneer="type") @grafanamaturity(NeedsExpertReview) + #LibraryPanelRef: { + name: string + uid: string + } @cuetsy(kind="interface") + #MatcherConfig: { id: string | *"" @grafanamaturity(NeedsExpertReview) options?: _ @grafanamaturity(NeedsExpertReview) diff --git a/kinds/librarypanel/librarypanel_kind.cue b/kinds/librarypanel/librarypanel_kind.cue new file mode 100644 index 00000000000..6a9cbd5c897 --- /dev/null +++ b/kinds/librarypanel/librarypanel_kind.cue @@ -0,0 +1,63 @@ +package kind + +import "strings" + +name: "LibraryPanel" +maturity: "experimental" + +lineage: seqs: [ + { + schemas: [ + // 0.0 + { + @grafana(TSVeneer="type") + + // Folder UID + folderUid?: string @grafanamaturity(ToMetadata="sys") + + // Library element UID + uid: string + + // Panel name (also saved in the model) + name: string & strings.MinRunes(1) + + // Panel description + description?: string + + // The panel type (from inside the model) + type: string & strings.MinRunes(1) + + // Dashboard version when this was saved (zero if unknown) + schemaVersion?: uint16 + + // panel version, incremented each time the dashboard is updated. + version: int64 @grafanamaturity(NeedsExpertReview) + + // TODO: should be the same panel schema defined in dashboard + // Typescript: Omit; + model: {...} + + // Object storage metadata + meta?: #LibraryElementDTOMeta @grafanamaturity(ToMetadata="sys") + + #LibraryElementDTOMetaUser: { + id: int64 + name: string + avatarUrl: string + } @cuetsy(kind="interface") @grafanamaturity(NeedsExpertReview) + + #LibraryElementDTOMeta: { + folderName: string + folderUid: string @grafanamaturity(ToMetadata="sys") + connectedDashboards: int64 + + created: string @grafanamaturity(ToMetadata="sys") // time.Time in golang + updated: string @grafanamaturity(ToMetadata="sys") // time.Time in golang + + createdBy: #LibraryElementDTOMetaUser @grafanamaturity(ToMetadata="sys") + updatedBy: #LibraryElementDTOMetaUser @grafanamaturity(ToMetadata="sys") + } @cuetsy(kind="interface") @grafanamaturity(NeedsExpertReview) + }, + ] + }, +] diff --git a/package.json b/package.json index 15ef5287b34..b48be2ee36c 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,9 @@ "e2e:debug": "./e2e/start-and-run-suite debug", "e2e:dev": "./e2e/start-and-run-suite dev", "e2e:benchmark:live": "./e2e/start-and-run-suite benchmark live", + "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", "test": "jest --notify --watch", "test:coverage": "jest --coverage", "test:coverage:changes": "jest --coverage --changedSince=origin/main", @@ -25,7 +28,7 @@ "packages:build": "lerna run build --ignore @grafana-plugins/input-datasource", "packages:clean": "rimraf ./npm-artifacts && lerna run clean --parallel", "packages:prepare": "lerna version --no-push --no-git-tag-version --force-publish --exact", - "packages:pack": "mkdir ./npm-artifacts && lerna exec --no-private -- yarn pack --out \"../../npm-artifacts/%s-%v.tgz\"", + "packages:pack": "mkdir -p ./npm-artifacts && lerna exec --no-private -- yarn pack --out \"../../npm-artifacts/%s-%v.tgz\"", "packages:publish": "lerna exec --no-private -- npm publish package.tgz", "packages:publishCanary": "lerna exec --no-private -- npm publish package.tgz --tag canary", "packages:publishLatest": "lerna exec --no-private -- npm publish package.tgz", @@ -112,7 +115,7 @@ "@rtsao/plugin-proposal-class-properties": "7.0.1-patch.1", "@swc/core": "1.3.11", "@swc/helpers": "0.4.12", - "@testing-library/dom": "8.19.0", + "@testing-library/dom": "8.20.0", "@testing-library/jest-dom": "5.16.5", "@testing-library/react": "12.1.4", "@testing-library/react-hooks": "8.0.1", @@ -121,8 +124,8 @@ "@types/angular-route": "1.7.2", "@types/common-tags": "^1.8.0", "@types/d3": "7.4.0", - "@types/d3-force": "^2.1.0", - "@types/d3-scale-chromatic": "1.3.1", + "@types/d3-force": "^3.0.0", + "@types/d3-scale-chromatic": "3.0.0", "@types/debounce-promise": "3.1.5", "@types/dompurify": "^2", "@types/eslint": "8.4.9", @@ -175,8 +178,9 @@ "babel-plugin-macros": "3.1.0", "blob-polyfill": "7.0.20220408", "browserslist": "^4.21.4", + "chance": "^1.0.10", "codeowners": "^5.1.1", - "copy-webpack-plugin": "9.0.1", + "copy-webpack-plugin": "11.0.0", "css-loader": "6.7.1", "css-minimizer-webpack-plugin": "4.2.2", "cypress": "9.5.1", @@ -267,7 +271,6 @@ "@grafana/scenes": "latest", "@grafana/schema": "workspace:*", "@grafana/ui": "workspace:*", - "@jaegertracing/jaeger-ui-components": "workspace:*", "@kusto/monaco-kusto": "5.3.6", "@leeoniya/ufuzzy": "0.9.1", "@lezer/common": "1.0.1", @@ -312,11 +315,12 @@ "calculate-size": "1.1.1", "centrifuge": "3.1.0", "classnames": "2.3.2", + "combokeys": "^3.0.0", "comlink": "4.3.1", "common-tags": "1.8.2", "core-js": "3.27.1", "d3": "7.8.2", - "d3-force": "2.1.1", + "d3-force": "3.0.0", "d3-scale-chromatic": "3.0.0", "dangerously-set-html-content": "1.0.9", "date-fns": "2.29.3", @@ -335,12 +339,14 @@ "immutable": "4.2.2", "jquery": "3.6.1", "js-yaml": "^4.1.0", + "json-markup": "^1.1.0", "json-source-map": "0.6.1", "jsurl": "^0.1.5", "kbar": "0.1.0-beta.36", "lodash": "4.17.21", "logfmt": "^1.3.2", "lru-cache": "7.14.0", + "lru-memoize": "^1.1.0", "memoize-one": "6.0.0", "moment": "2.29.4", "moment-timezone": "0.5.38", @@ -357,19 +363,19 @@ "prop-types": "15.8.1", "pseudoizer": "^0.1.0", "rc-cascader": "3.8.0", - "rc-drawer": "6.0.1", - "rc-slider": "10.0.1", + "rc-drawer": "6.1.2", + "rc-slider": "10.1.0", "rc-time-picker": "3.7.3", "rc-tree": "5.7.0", "re-resizable": "6.9.9", "react": "17.0.2", - "react-awesome-query-builder": "5.3.1", + "react-awesome-query-builder": "5.4.0", "react-beautiful-dnd": "13.1.1", "react-diff-viewer": "^3.1.1", "react-dom": "17.0.2", "react-draggable": "4.4.5", "react-grid-layout": "1.3.4", - "react-highlight-words": "0.18.0", + "react-highlight-words": "0.20.0", "react-hook-form": "7.5.3", "react-i18next": "^12.0.0", "react-inlinesvg": "3.0.1", @@ -385,6 +391,7 @@ "react-table": "7.8.0", "react-transition-group": "4.4.5", "react-use": "17.4.0", + "react-virtual": "2.8.2", "react-virtualized-auto-sizer": "1.0.7", "react-window": "1.8.8", "react-window-infinite-loader": "1.0.8", @@ -406,7 +413,8 @@ "tether-drop": "https://github.com/torkelo/drop", "tinycolor2": "1.4.2", "tslib": "2.4.1", - "uplot": "1.6.23", + "tween-functions": "^1.2.0", + "uplot": "1.6.24", "uuid": "9.0.0", "vendor": "link:./public/vendor", "visjs-network": "4.25.0", @@ -430,7 +438,6 @@ "@storybook/builder-webpack5/webpack": "5.74.0", "@storybook/manager-webpack5/webpack": "5.74.0", "ngtemplate-loader/loader-utils": "^2.0.0", - "node-fetch": "2.6.7", "slate-dev-environment@^0.2.2": "patch:slate-dev-environment@npm:0.2.5#.yarn/patches/slate-dev-environment-npm-0.2.5-9aeb7da7b5.patch" }, "workspaces": { diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index 1d6ac0f86a5..27a713d6c83 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -37,7 +37,7 @@ "dependencies": { "@braintree/sanitize-url": "6.0.1", "@grafana/schema": "9.4.0-pre", - "@types/d3-interpolate": "^1.4.0", + "@types/d3-interpolate": "^3.0.0", "d3-interpolate": "3.0.1", "date-fns": "2.29.3", "eventemitter3": "4.0.7", @@ -54,7 +54,7 @@ "rxjs": "7.5.7", "tinycolor2": "1.4.2", "tslib": "2.4.1", - "uplot": "1.6.23", + "uplot": "1.6.24", "xss": "1.0.14" }, "devDependencies": { @@ -62,7 +62,7 @@ "@rollup/plugin-commonjs": "23.0.2", "@rollup/plugin-json": "5.0.1", "@rollup/plugin-node-resolve": "15.0.1", - "@testing-library/dom": "8.19.0", + "@testing-library/dom": "8.20.0", "@testing-library/jest-dom": "5.16.5", "@testing-library/react": "12.1.4", "@testing-library/react-hooks": "8.0.1", diff --git a/packages/grafana-data/src/transformations/transformers/ensureColumns.ts b/packages/grafana-data/src/transformations/transformers/ensureColumns.ts index 9cca6fead37..4968336c236 100644 --- a/packages/grafana-data/src/transformations/transformers/ensureColumns.ts +++ b/packages/grafana-data/src/transformations/transformers/ensureColumns.ts @@ -20,13 +20,12 @@ export const ensureColumnsTransformer: SynchronousDataTransformerInfo = { const timeFieldName = findConsistentTimeFieldName(frames); if (frames.length > 1 && timeFieldName) { - const fields: { [key: string]: string } = {}; - for (const frame of frames) { - if (frame.refId) { - fields[frame.refId] = timeFieldName; - } - } - return joinByFieldTransformer.transformer({ fields }, ctx)(frames); + return joinByFieldTransformer.transformer( + { + byField: timeFieldName, + }, + ctx + )(frames); } return frames; }, diff --git a/packages/grafana-data/src/transformations/transformers/joinByField.test.ts b/packages/grafana-data/src/transformations/transformers/joinByField.test.ts index 5326e86da46..1a1026017b3 100644 --- a/packages/grafana-data/src/transformations/transformers/joinByField.test.ts +++ b/packages/grafana-data/src/transformations/transformers/joinByField.test.ts @@ -15,7 +15,6 @@ describe('JOIN Transformer', () => { describe('outer join', () => { const everySecondSeries = toDataFrame({ name: 'even', - refId: 'even', fields: [ { name: 'time', type: FieldType.time, values: [3000, 4000, 5000, 6000] }, { name: 'temperature', type: FieldType.number, values: [10.3, 10.4, 10.5, 10.6] }, @@ -25,7 +24,6 @@ describe('JOIN Transformer', () => { const everyOtherSecondSeries = toDataFrame({ name: 'odd', - refId: 'odd', fields: [ { name: 'time', type: FieldType.time, values: [1000, 3000, 5000, 7000] }, { name: 'temperature', type: FieldType.number, values: [11.1, 11.3, 11.5, 11.7] }, @@ -35,12 +33,9 @@ describe('JOIN Transformer', () => { it('joins by time field', async () => { const cfg: DataTransformerConfig = { - id: DataTransformerID.joinByField, + id: DataTransformerID.seriesToColumns, options: { - fields: { - even: 'time', - odd: 'time', - }, + byField: 'time', }, }; @@ -140,12 +135,9 @@ describe('JOIN Transformer', () => { it('joins by temperature field', async () => { const cfg: DataTransformerConfig = { - id: DataTransformerID.joinByField, + id: DataTransformerID.seriesToColumns, options: { - fields: { - even: 'temperature', - odd: 'temperature', - }, + byField: 'temperature', }, }; @@ -153,7 +145,6 @@ describe('JOIN Transformer', () => { (received) => { const data = received[0]; const filtered = data[0]; - expect(filtered.fields).toMatchInlineSnapshot(` [ { @@ -260,12 +251,9 @@ describe('JOIN Transformer', () => { it('joins by time field in reverse order', async () => { const cfg: DataTransformerConfig = { - id: DataTransformerID.joinByField, + id: DataTransformerID.seriesToColumns, options: { - fields: { - even: 'time', - odd: 'time', - }, + byField: 'time', }, }; @@ -277,7 +265,6 @@ describe('JOIN Transformer', () => { (received) => { const data = received[0]; const filtered = data[0]; - expect(filtered.fields).toMatchInlineSnapshot(` [ { @@ -389,12 +376,9 @@ describe('JOIN Transformer', () => { it('when dataframe and field share the same name then use the field name', async () => { const cfg: DataTransformerConfig = { - id: DataTransformerID.joinByField, + id: DataTransformerID.seriesToColumns, options: { - fields: { - even: 'time', - odd: 'time', - }, + byField: 'time', }, }; @@ -455,12 +439,9 @@ describe('JOIN Transformer', () => { it('joins if fields are missing', async () => { const cfg: DataTransformerConfig = { - id: DataTransformerID.joinByField, + id: DataTransformerID.seriesToColumns, options: { - fields: { - even: 'time', - odd: 'time', - }, + byField: 'time', }, }; @@ -536,12 +517,9 @@ describe('JOIN Transformer', () => { it('handles duplicate field name', async () => { const cfg: DataTransformerConfig = { - id: DataTransformerID.joinByField, + id: DataTransformerID.seriesToColumns, options: { - fields: { - even: 'time', - odd: 'time', - }, + byField: 'time', }, }; @@ -602,7 +580,6 @@ describe('JOIN Transformer', () => { describe('inner join', () => { const seriesA = toDataFrame({ name: 'A', - refId: 'A', fields: [ { name: 'time', type: FieldType.time, values: [3000, 4000, 5000, 6000] }, { name: 'temperature', type: FieldType.number, values: [10.3, 10.4, 10.5, 10.6] }, @@ -612,7 +589,6 @@ describe('JOIN Transformer', () => { const seriesB = toDataFrame({ name: 'B', - refId: 'B', fields: [ { name: 'time', type: FieldType.time, values: [1000, 3000, 5000, 7000] }, { name: 'temperature', type: FieldType.number, values: [11.1, 10.3, 10.5, 11.7] }, @@ -622,12 +598,9 @@ describe('JOIN Transformer', () => { it('inner joins by time field', async () => { const cfg: DataTransformerConfig = { - id: DataTransformerID.joinByField, + id: DataTransformerID.seriesToColumns, options: { - fields: { - A: 'time', - B: 'time', - }, + byField: 'time', mode: JoinMode.inner, }, }; @@ -706,12 +679,9 @@ describe('JOIN Transformer', () => { it('inner joins by temperature field', async () => { const cfg: DataTransformerConfig = { - id: DataTransformerID.joinByField, + id: DataTransformerID.seriesToColumns, options: { - fields: { - A: 'temperature', - B: 'temperature', - }, + byField: 'temperature', mode: JoinMode.inner, }, }; @@ -794,12 +764,9 @@ describe('JOIN Transformer', () => { it('inner joins by time field in reverse order', async () => { const cfg: DataTransformerConfig = { - id: DataTransformerID.joinByField, + id: DataTransformerID.seriesToColumns, options: { - fields: { - A: 'time', - B: 'time', - }, + byField: 'time', mode: JoinMode.inner, }, }; @@ -885,7 +852,6 @@ describe('JOIN Transformer', () => { describe('Field names', () => { const seriesWithSameFieldAndDataFrameName = toDataFrame({ name: 'temperature', - refId: 'temperature', fields: [ { name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] }, { name: 'temperature', type: FieldType.number, values: [1, 3, 5, 7] }, @@ -894,7 +860,6 @@ describe('JOIN Transformer', () => { const seriesB = toDataFrame({ name: 'B', - refId: 'B', fields: [ { name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] }, { name: 'temperature', type: FieldType.number, values: [2, 4, 6, 8] }, @@ -903,12 +868,9 @@ describe('JOIN Transformer', () => { it('when dataframe and field share the same name then use the field name', async () => { const cfg: DataTransformerConfig = { - id: DataTransformerID.joinByField, + id: DataTransformerID.seriesToColumns, options: { - fields: { - temperature: 'time', - B: 'time', - }, + byField: 'time', mode: JoinMode.inner, }, }; @@ -970,20 +932,15 @@ describe('JOIN Transformer', () => { it('joins if fields are missing', async () => { const cfg: DataTransformerConfig = { - id: DataTransformerID.joinByField, + id: DataTransformerID.seriesToColumns, options: { - fields: { - A: 'time', - B: 'time', - C: 'time', - }, + byField: 'time', mode: JoinMode.inner, }, }; const frame1 = toDataFrame({ name: 'A', - refId: 'A', fields: [ { name: 'time', type: FieldType.time, values: [1, 2, 3] }, { name: 'temperature', type: FieldType.number, values: [10, 11, 12] }, @@ -992,13 +949,11 @@ describe('JOIN Transformer', () => { const frame2 = toDataFrame({ name: 'B', - refId: 'B', fields: [], }); const frame3 = toDataFrame({ name: 'C', - refId: 'C', fields: [ { name: 'time', type: FieldType.time, values: [1, 2, 3] }, { name: 'temperature', type: FieldType.number, values: [20, 22, 24] }, @@ -1056,18 +1011,14 @@ describe('JOIN Transformer', () => { it('handles duplicate field name', async () => { const cfg: DataTransformerConfig = { - id: DataTransformerID.joinByField, + id: DataTransformerID.seriesToColumns, options: { - fields: { - frame1: 'time', - frame2: 'time', - }, + byField: 'time', mode: JoinMode.inner, }, }; const frame1 = toDataFrame({ - refId: 'frame1', fields: [ { name: 'time', type: FieldType.time, values: [1] }, { name: 'temperature', type: FieldType.number, values: [10] }, @@ -1075,7 +1026,6 @@ describe('JOIN Transformer', () => { }); const frame2 = toDataFrame({ - refId: 'frame2', fields: [ { name: 'time', type: FieldType.time, values: [1] }, { name: 'temperature', type: FieldType.number, values: [20] }, diff --git a/packages/grafana-data/src/transformations/transformers/joinByField.ts b/packages/grafana-data/src/transformations/transformers/joinByField.ts index 2946dd4125b..e6c1d386fa6 100644 --- a/packages/grafana-data/src/transformations/transformers/joinByField.ts +++ b/packages/grafana-data/src/transformations/transformers/joinByField.ts @@ -1,6 +1,8 @@ import { map } from 'rxjs/operators'; -import { DataFrame, SynchronousDataTransformerInfo } from '../../types'; +import { DataFrame, SynchronousDataTransformerInfo, FieldMatcher } from '../../types'; +import { fieldMatchers } from '../matchers'; +import { FieldMatcherID } from '../matchers/ids'; import { DataTransformerID } from './ids'; import { joinDataFrames } from './joinDataFrames'; @@ -11,7 +13,7 @@ export enum JoinMode { } export interface JoinByFieldOptions { - fields?: { [key: string]: string }; // empty will pick the field automatically + byField?: string; // empty will pick the field automatically mode?: JoinMode; } @@ -22,7 +24,7 @@ export const joinByFieldTransformer: SynchronousDataTransformerInfo joinByFieldTransformer.transformer(options, ctx)(data))), transformer: (options: JoinByFieldOptions) => { + let joinBy: FieldMatcher | undefined = undefined; return (data: DataFrame[]) => { if (data.length > 1) { - const joined = joinDataFrames({ frames: data, mode: options.mode, fields: options.fields }); + if (options.byField && !joinBy) { + joinBy = fieldMatchers.get(FieldMatcherID.byName).get(options.byField); + } + const joined = joinDataFrames({ frames: data, joinBy, mode: options.mode }); if (joined) { return [joined]; } } - return data; }; }, diff --git a/packages/grafana-data/src/transformations/transformers/joinDataFrames.test.ts b/packages/grafana-data/src/transformations/transformers/joinDataFrames.test.ts index 83dd9469f6b..5f4ef23427b 100644 --- a/packages/grafana-data/src/transformations/transformers/joinDataFrames.test.ts +++ b/packages/grafana-data/src/transformations/transformers/joinDataFrames.test.ts @@ -359,103 +359,4 @@ describe('align frames', () => { expect(isLikelyAscendingVector(new ArrayVector([null, 1, null]), 3)).toBeTruthy(); }); }); - - describe('should perform a join on custom fields', () => { - const tags = toDataFrame({ - refId: 'tags', - fields: [ - { name: 'tags__time', type: FieldType.time, values: [100, 101, 200] }, - { name: 'tags__name', type: FieldType.string, values: ['v1.2', 'v1.2b', 'v1.3'] }, - ], - }); - - const releases = toDataFrame({ - refId: 'releases', - fields: [ - { name: 'releases__time', type: FieldType.time, values: [150, 250] }, - { name: 'releases__tag', type: FieldType.string, values: ['v1.2', 'v1.3'] }, - ], - }); - - const features = toDataFrame({ - refId: 'features', - fields: [ - { name: 'features__name', type: FieldType.string, values: ['A', 'B', 'C', 'D', 'E'] }, - { name: 'features__tag', type: FieldType.time, values: ['v1.2', 'v1.3', 'v1.2b', 'v1.3', 'v1.2'] }, - ], - }); - - it('should perform an outer join', () => { - const out = joinDataFrames({ - frames: [tags, releases, features], - fields: { - tags: 'tags__name', - releases: 'releases__tag', - features: 'features__tag', - }, - })!; - - expect( - out.fields.map((f) => ({ - name: f.name, - values: f.values.toArray(), - })) - ).toEqual([ - { - name: 'tags__name', - values: ['v1.2', 'v1.2b', 'v1.3'], - }, - { - name: 'tags__time', - values: [100, 101, 200], - }, - { - name: 'releases__time', - values: [150, undefined, 250], - }, - { - name: 'features__name', - values: ['E', 'C', 'D'], - }, - ]); - }); - - it('should perform an inner join', () => { - const out = joinDataFrames({ - frames: [tags, releases, features], - fields: { - tags: 'tags__name', - releases: 'releases__tag', - features: 'features__tag', - }, - mode: JoinMode.inner, - })!; - - const mappedOut = out.fields.map((f) => ({ - name: f.name, - values: f.values.toArray(), - })); - - const expected = [ - { - name: 'tags__name', - values: ['v1.2', 'v1.3'], - }, - { - name: 'tags__time', - values: [100, 200], - }, - { - name: 'releases__time', - values: [150, 250], - }, - { - name: 'features__name', - values: ['E', 'D'], - }, - ]; - - expect(JSON.stringify(mappedOut)).toEqual(JSON.stringify(expected)); - }); - }); }); diff --git a/packages/grafana-data/src/transformations/transformers/joinDataFrames.ts b/packages/grafana-data/src/transformations/transformers/joinDataFrames.ts index dd09c373bfb..767161d2f9a 100644 --- a/packages/grafana-data/src/transformations/transformers/joinDataFrames.ts +++ b/packages/grafana-data/src/transformations/transformers/joinDataFrames.ts @@ -47,11 +47,6 @@ export interface JoinOptions { */ joinBy?: FieldMatcher; - /** - * The fields to join on - */ - fields?: { [key: string]: string }; - /** * Optionally filter the non-join fields */ @@ -68,16 +63,8 @@ export interface JoinOptions { mode?: JoinMode; } -function getJoinMatcher(options: JoinOptions, refId: string | undefined): FieldMatcher { - if (options.joinBy) { - return options.joinBy; - } - - if (!options.fields || !refId) { - return pickBestJoinField(options.frames); - } - - return fieldMatchers.get(FieldMatcherID.byName).get(options.fields[refId]); +function getJoinMatcher(options: JoinOptions): FieldMatcher { + return options.joinBy ?? pickBestJoinField(options.frames); } /** @@ -108,7 +95,7 @@ export function joinDataFrames(options: JoinOptions): DataFrame | undefined { let frame = options.frames[0]; let frameCopy = frame; - const joinFieldMatcher = getJoinMatcher(options, frame.refId); + const joinFieldMatcher = getJoinMatcher(options); let joinIndex = frameCopy.fields.findIndex((f) => joinFieldMatcher(f, frameCopy, options.frames)); if (options.keepOriginIndices) { @@ -165,10 +152,10 @@ export function joinDataFrames(options: JoinOptions): DataFrame | undefined { const nullModes: JoinNullMode[][] = []; const allData: AlignedData[] = []; const originalFields: Field[] = []; + const joinFieldMatcher = getJoinMatcher(options); for (let frameIndex = 0; frameIndex < options.frames.length; frameIndex++) { const frame = options.frames[frameIndex]; - const joinFieldMatcher = getJoinMatcher(options, frame.refId); if (!frame || !frame.fields?.length) { continue; // skip the frame diff --git a/packages/grafana-data/src/types/annotations.ts b/packages/grafana-data/src/types/annotations.ts index 099ad4fe034..da077daa76e 100644 --- a/packages/grafana-data/src/types/annotations.ts +++ b/packages/grafana-data/src/types/annotations.ts @@ -113,4 +113,9 @@ export interface AnnotationSupport>; + + /** + * Define this method if you want to pre-populate the editor with a default query + */ + getDefaultQuery?(): Partial; } diff --git a/packages/grafana-data/src/types/app.ts b/packages/grafana-data/src/types/app.ts index aeb336fb15f..f1c3eda327a 100644 --- a/packages/grafana-data/src/types/app.ts +++ b/packages/grafana-data/src/types/app.ts @@ -52,7 +52,6 @@ export interface AppPluginMeta extends PluginMeta export class AppPlugin extends GrafanaPlugin> { // Content under: /a/${plugin-id}/* root?: ComponentType>; - rootNav?: NavModel; // Initial navigation model /** * Called after the module has loaded, and before the app is used. @@ -66,12 +65,9 @@ export class AppPlugin extends GrafanaPlugin>, rootNav?: NavModel) { + setRootPage(root: ComponentType>) { this.root = root; - this.rootNav = rootNav; return this; } diff --git a/packages/grafana-data/src/types/config.ts b/packages/grafana-data/src/types/config.ts index 804fa0b2601..d4c9381f6f7 100644 --- a/packages/grafana-data/src/types/config.ts +++ b/packages/grafana-data/src/types/config.ts @@ -117,7 +117,7 @@ export interface CurrentUserDTO { login: string; email: string; name: string; - lightTheme: boolean; + theme: string; // dark | light | system orgCount: number; orgId: number; orgName: string; @@ -129,6 +129,9 @@ export interface CurrentUserDTO { locale: string; language: string; permissions?: Record; + + /** @deprecated Use theme instead */ + lightTheme: boolean; } /** Contains essential user and config info @@ -229,6 +232,7 @@ export interface AuthSettings { GrafanaComSkipOrgRoleSync?: boolean; GithubSkipOrgRoleSync?: boolean; GitLabSkipOrgRoleSync?: boolean; + OktaSkipOrgRoleSync?: boolean; AzureADSkipOrgRoleSync?: boolean; GoogleSkipOrgRoleSync?: boolean; DisableSyncLock?: boolean; diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index aaf4f957601..126aebe083f 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -47,7 +47,6 @@ export interface FeatureToggles { supportBundles?: boolean; dashboardsFromStorage?: boolean; export?: boolean; - azureMonitorResourcePickerForMetrics?: boolean; exploreMixedDatasource?: boolean; tracing?: boolean; commandPalette?: boolean; @@ -90,6 +89,7 @@ export interface FeatureToggles { alertingBacktesting?: boolean; editPanelCSVDragAndDrop?: boolean; alertingNoNormalState?: boolean; - azureMultipleResourcePicker?: boolean; topNavCommandPalette?: boolean; + logsSampleInExplore?: boolean; + logsContextDatasourceUi?: boolean; } diff --git a/packages/grafana-data/src/types/logs.ts b/packages/grafana-data/src/types/logs.ts index 6469be8404a..6d33e89db4d 100644 --- a/packages/grafana-data/src/types/logs.ts +++ b/packages/grafana-data/src/types/logs.ts @@ -167,6 +167,13 @@ export interface DataSourceWithLogsContextSupport void): React.ReactNode; } export const hasLogsContextSupport = (datasource: unknown): datasource is DataSourceWithLogsContextSupport => { @@ -230,3 +237,13 @@ export const hasSupplementaryQuerySupport = ( withSupplementaryQueriesSupport.getSupportedSupplementaryQueryTypes().includes(type) ); }; + +export const hasLogsContextUiSupport = (datasource: unknown): datasource is DataSourceWithLogsContextSupport => { + if (!datasource) { + return false; + } + + const withLogsSupport = datasource as DataSourceWithLogsContextSupport; + + return withLogsSupport.getLogRowContextUi !== undefined; +}; diff --git a/packages/grafana-data/src/types/variables.ts b/packages/grafana-data/src/types/variables.ts index a7d3087d8ff..f8d8666c09e 100644 --- a/packages/grafana-data/src/types/variables.ts +++ b/packages/grafana-data/src/types/variables.ts @@ -35,6 +35,11 @@ export abstract class VariableSupportBase< TOptions extends DataSourceJsonData = DataSourceOptionsType > { abstract getType(): VariableSupportType; + + /** + * Define this method in the config if you want to pre-populate the editor with a default query. + */ + getDefaultQuery?(): Partial; } /** diff --git a/packages/grafana-runtime/package.json b/packages/grafana-runtime/package.json index b39f8343576..41a95599839 100644 --- a/packages/grafana-runtime/package.json +++ b/packages/grafana-runtime/package.json @@ -52,7 +52,7 @@ "@grafana/tsconfig": "^1.2.0-rc1", "@rollup/plugin-commonjs": "23.0.2", "@rollup/plugin-node-resolve": "15.0.1", - "@testing-library/dom": "8.19.0", + "@testing-library/dom": "8.20.0", "@testing-library/react": "12.1.4", "@testing-library/react-hooks": "8.0.1", "@testing-library/user-event": "14.4.3", diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts index 1382f8c6aba..c53b35d0fbb 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -111,6 +111,7 @@ export class GrafanaBootConfig implements GrafanaConfig { pluginAdminEnabled = true; pluginAdminExternalManageEnabled = false; pluginCatalogHiddenPlugins: string[] = []; + pluginsCDNBaseURL = ''; expressionsEnabled = false; customTheme?: undefined; awsAllowedAuthProviders: string[] = []; @@ -148,6 +149,7 @@ export class GrafanaBootConfig implements GrafanaConfig { constructor(options: GrafanaBootConfig) { this.bootData = options.bootData; + this.bootData.user.lightTheme = getThemeMode(options) === 'light'; this.isPublicDashboardView = options.bootData.settings.isPublicDashboardView; const defaults = { @@ -188,8 +190,24 @@ export class GrafanaBootConfig implements GrafanaConfig { } } +function getThemeMode(config: GrafanaBootConfig) { + let mode: 'light' | 'dark' = 'dark'; + const themePref = config.bootData.user.theme; + + if (themePref === 'light' || themePref === 'dark') { + mode = themePref; + } else if (themePref === 'system') { + const mediaResult = window.matchMedia('(prefers-color-scheme: dark)'); + mode = mediaResult.matches ? 'dark' : 'light'; + } + + return mode; +} + function getThemeCustomizations(config: GrafanaBootConfig) { + // if/when we remove CurrentUserDTO.lightTheme, change this to use getThemeMode instead const mode = config.bootData.user.lightTheme ? 'light' : 'dark'; + const themeOptions: NewThemeOptions = { colors: { mode }, }; diff --git a/packages/grafana-schema/src/index.gen.ts b/packages/grafana-schema/src/index.gen.ts index a7a76a0c3df..f37d525a6ad 100644 --- a/packages/grafana-schema/src/index.gen.ts +++ b/packages/grafana-schema/src/index.gen.ts @@ -26,6 +26,7 @@ export type { SpecialValueMap, ValueMappingResult, Transformation, + LibraryPanelRef, RowPanel, GraphPanel, HeatmapPanel @@ -86,6 +87,23 @@ export { defaultFieldConfig } from './veneer/dashboard.types'; +// Raw generated types from LibraryPanel kind. +export type { + LibraryElementDTOMetaUser, + LibraryElementDTOMeta +} from './raw/librarypanel/x/librarypanel_types.gen'; + +// The following exported declarations correspond to types in the librarypanel@0.0 kind's +// schema with attribute @grafana(TSVeneer="type"). +// +// The handwritten file for these type and default veneers is expected to be at +// packages/grafana-schema/src/veneer/librarypanel.types.ts. +// This re-export declaration enforces that the handwritten veneer file exists, +// and exports all the symbols in the list. +// +// TODO generate code such that tsc enforces type compatibility between raw and veneer decls +export type { LibraryPanel } from './veneer/librarypanel.types'; + // Raw generated types from Playlist kind. export type { Playlist, diff --git a/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts b/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts index bbaaee013f0..95b22b152ce 100644 --- a/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts +++ b/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts @@ -404,6 +404,10 @@ export interface Panel { * TODO tighter constraint */ interval?: string; + /** + * Dynamically load the panel + */ + libraryPanel?: LibraryPanelRef; /** * Panel links. * TODO fill this out - seems there are a couple variants? @@ -503,6 +507,11 @@ export const defaultFieldConfigSource: Partial = { overrides: [], }; +export interface LibraryPanelRef { + name: string; + uid: string; +} + export interface MatcherConfig { id: string; options?: unknown; @@ -777,9 +786,9 @@ export interface Dashboard { time_options: Array; }; /** - * Timezone of dashboard, + * Timezone of dashboard. Accepts IANA TZDB zone ID or "browser" or "utc". */ - timezone?: ('browser' | 'utc' | ''); + timezone?: string; /** * Title of dashboard. */ diff --git a/packages/grafana-schema/src/raw/librarypanel/x/librarypanel_types.gen.ts b/packages/grafana-schema/src/raw/librarypanel/x/librarypanel_types.gen.ts new file mode 100644 index 00000000000..97cba2a5c5e --- /dev/null +++ b/packages/grafana-schema/src/raw/librarypanel/x/librarypanel_types.gen.ts @@ -0,0 +1,65 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. +// +// Generated by: +// kinds/gen.go +// Using jennies: +// TSTypesJenny +// LatestMajorsOrXJenny +// +// Run 'make gen-cue' from repository root to regenerate. + +export interface LibraryElementDTOMetaUser { + avatarUrl: string; + id: number; + name: string; +} + +export interface LibraryElementDTOMeta { + connectedDashboards: number; + created: string; + createdBy: LibraryElementDTOMetaUser; + folderName: string; + folderUid: string; + updated: string; + updatedBy: LibraryElementDTOMetaUser; +} + +export interface LibraryPanel { + /** + * Panel description + */ + description?: string; + /** + * Folder UID + */ + folderUid?: string; + /** + * Object storage metadata + */ + meta?: LibraryElementDTOMeta; + /** + * TODO: should be the same panel schema defined in dashboard + * Typescript: Omit; + */ + model: Record; + /** + * Panel name (also saved in the model) + */ + name: string; + /** + * Dashboard version when this was saved (zero if unknown) + */ + schemaVersion?: number; + /** + * The panel type (from inside the model) + */ + type: string; + /** + * Library element UID + */ + uid: string; + /** + * panel version, incremented each time the dashboard is updated. + */ + version: number; +} diff --git a/packages/grafana-schema/src/veneer/librarypanel.types.ts b/packages/grafana-schema/src/veneer/librarypanel.types.ts new file mode 100644 index 00000000000..007bb085742 --- /dev/null +++ b/packages/grafana-schema/src/veneer/librarypanel.types.ts @@ -0,0 +1,7 @@ +import * as raw from '../raw/librarypanel/x/librarypanel_types.gen'; + +import { Panel } from './dashboard.types'; + +export interface LibraryPanel extends raw.LibraryPanel { + model: Omit; +} diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/Dockerfile b/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/Dockerfile deleted file mode 100644 index 3907fe9c05a..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/Dockerfile +++ /dev/null @@ -1,10 +0,0 @@ -FROM alpine:3.15.6 - -USER root - -COPY scripts scripts -COPY install /usr/local - -WORKDIR scripts - -RUN ./deploy.sh diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/README.md b/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/README.md deleted file mode 100644 index 94b42aefd3d..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/README.md +++ /dev/null @@ -1,71 +0,0 @@ -# Using this docker image - -Uploaded to dockerhub as grafana/grafana-plugin-ci:latest-alpine - -Based off of `circleci/node:12-browsers` - -## User - -The user will be `circleci` -The home directory will be `/home/circleci` - -## Node - -- node 12 is installed -- yarn is installed globally -- npm is installed globally - -## Go - -- Go is installed in `/usr/local/bin/go` -- golangci-lint is installed in `/usr/local/bin/golangci-lint` -- mage is installed in `/home/circleci/go/bin/mage` - -All of the above directories are in the path, so there is no need to specify fully qualified paths. - -## Grafana - -- Installed in `/home/circleci/src/grafana` -- `yarn install` has been run - -## Integration/Release Testing - -There are 4 previous versions pre-downloaded to /usr/local/grafana. These versions are: - -1. 6.6.2 -2. 6.5.3 -3. 6.4.5 -4. 6.3.7 - -To test, your CircleCI config will need a run section with something similar to the following - -``` -- run: - name: Setup Grafana (local install) - command: | - sudo dpkg -i /usr/local/grafana/deb/grafana_6.6.2_amd64.deb - sudo cp ci/grafana-test-env/custom.ini /usr/share/grafana/conf/custom.ini - sudo cp ci/grafana-test-env/custom.ini /etc/grafana/grafana.ini - sudo service grafana-server start - grafana-cli --version -``` - -# Building - -To build, cd to `/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine` - -``` -./build.sh -``` - -# Developing/Testing - -To test, you should have docker-compose installed. - -``` -cd test -./start.sh -``` - -You will be in /home/circleci/test with the buildscripts installed to the local directory. -Do your edits/run tests. When saving, your edits will be available in the container immediately. diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/build.sh b/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/build.sh deleted file mode 100755 index f024e617055..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/build.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash -set -eo pipefail - -source ./common.sh - -# -# No longer required, but useful to keep just in case we want to deploy -# changes in toolkit directly to the docker image -# -if [ -n "$INCLUDE_TOOLKIT" ]; then - /bin/rm -rfv install/grafana-toolkit - mkdir -pv install/grafana-toolkit - cp -rv ../../bin install/grafana-toolkit - cp -rv ../../src install/grafana-toolkit - cp -v ../../package.json install/grafana-toolkit - cp -v ../../tsconfig.json install/grafana-toolkit -fi - -docker build -t ${DOCKER_IMAGE_NAME} . -docker push $DOCKER_IMAGE_NAME - -[ -n "$INCLUDE_TOOLKIT" ] && /bin/rm -rfv install/grafana-toolkit diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/common.sh b/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/common.sh deleted file mode 100755 index 8a99e1e649b..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/common.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -## -## Common variable declarations -## Find the latest tags on https://hub.docker.com/r/grafana/grafana-plugin-ci/tags?page=1&name=alpine -## - -DOCKER_IMAGE_NAME="grafana/grafana-plugin-ci:1.6.1-alpine" diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/install/bin/cp b/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/install/bin/cp deleted file mode 100755 index fd23c3db871..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/install/bin/cp +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/sh - -if [ "$1" == "-rn" ]; then - false | busybox cp -i -r "$2" "$3" 2>/dev/null -else - busybox cp $* -fi diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/install/bin/ginstall b/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/install/bin/ginstall deleted file mode 100755 index 537305a1f72..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/install/bin/ginstall +++ /dev/null @@ -1,73 +0,0 @@ -#!/bin/sh -## -# gget -# A script to get and install grafana versions -# for usage information see "show_help" below. -# - -latest=$(wget -O - 'https://raw.githubusercontent.com/grafana/grafana/main/latest.json' | jq -r '.stable') -canary=$(wget -O - "https://grafana.com/api/grafana/versions" | jq ".items[0].version" | tr -d '"') - -show_help() { - echo "Usage: gget " - echo "" - echo "where can be:" - echo " 1) A version from https://grafana.com/grafana/download (ex x.y.z)" - echo " 2) latest (currently $latest)" - echo " 3) canary (currently $canary)" - echo "" - echo " -h, --help: Display this help message" - echo "" - exit 0 -} - -opts=$(getopt -o h --long help -n 'gget' -- "$@") -[ $? -eq 0 ] || { - show_help -} - -eval set -- "$opts" -while true; do - case "$1" in - -h | --help) - show_help - ;; - --) - shift - break - ;; - *) - break - ;; - esac - shift -done - -[ -z "$1" ] && show_help - -# Make sure the script is being run as root -if [ $EUID -ne 0 ]; then - echo "This script must be run as root" - exit 1 -fi - -## -# MAIN -# -# Enough setup, let's actually do something -# -version=$1 -if [ "$version" == "latest" ]; then - version="$latest" - wget -O - "https://dl.grafana.com/oss/release/grafana-${version}.linux-amd64.tar.gz" | tar -C /opt -zxf - -elif [ "$version" == "canary" ]; then - version="$canary" - wget -O - "https://dl.grafana.com/oss/main/grafana-${version}.linux-amd64.tar.gz" | tar -C /opt -zxf - -else - wget -O - "https://dl.grafana.com/oss/release/grafana-${version}.linux-amd64.tar.gz" | tar -C /opt -zxf - -fi - -/bin/rm -rf /opt/grafana > /dev/null 2>&1 || true -ln -s /opt/grafana-${version} /opt/grafana - -# nohup /opt/grafana/bin/grafana-server -config /opt/grafana/conf/defaults.ini -homepath /opt/grafana >/dev/null 2>&1 & diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/install/bin/githubRelease.js b/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/install/bin/githubRelease.js deleted file mode 100644 index 849836da380..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/install/bin/githubRelease.js +++ /dev/null @@ -1,154 +0,0 @@ -'use strict'; -Object.defineProperty(exports, '__esModule', { value: true }); -const fs = require('fs'); -const path = require('path'); -const tslib_1 = require('tslib'); - -const getPluginId_1 = require('../../config/utils/getPluginId'); -const pluginValidation_1 = require('../../config/utils/pluginValidation'); -const env_1 = require('../../plugins/env'); -// @ts-ignore -// import execa = require('execa'); -const githubClient_1 = tslib_1.__importDefault(require('./githubClient')); -const resolveContentType = function (extension) { - if (extension.startsWith('.')) { - extension = extension.slice(1); - } - switch (extension) { - case 'zip': - return 'application/zip'; - case 'json': - return 'application/json'; - case 'sha1': - return 'text/plain'; - default: - return 'application/octet-stream'; - } -}; -const GitHubRelease = /** @class */ (function () { - function GitHubRelease(token, username, repository, releaseNotes, commitHash) { - this.token = token; - this.username = username; - this.repository = repository; - this.releaseNotes = releaseNotes; - this.commitHash = commitHash; - this.git = new githubClient_1.default({ - required: true, - repo: repository, - }); - } - GitHubRelease.prototype.publishAssets = function (srcLocation, destUrl) { - const _this = this; - // Add the assets. Loop through files in the ci/dist folder and upload each asset. - const files = fs.readdirSync(srcLocation); - return files.map(function (file) { - return tslib_1.__awaiter(_this, void 0, void 0, function () { - let fileStat, fileData; - return tslib_1.__generator(this, function (_a) { - fileStat = fs.statSync(srcLocation + '/' + file); - fileData = fs.readFileSync(srcLocation + '/' + file); - return [ - 2 /*return*/, - this.git.client.post(destUrl + '?name=' + file, fileData, { - headers: { - 'Content-Type': resolveContentType(path.extname(file)), - 'Content-Length': fileStat.size, - }, - maxContentLength: fileStat.size * 2 * 1024 * 1024, - }), - ]; - }); - }); - }); - }; - GitHubRelease.prototype.release = function () { - let _a, _b, _c, _d; - return tslib_1.__awaiter(this, void 0, void 0, function () { - let ciDir, - distDir, - distContentDir, - pluginJsonFile, - pluginInfo, - PUBLISH_DIR, - commitHash, - latestRelease, - reason_1, - newReleaseResponse, - publishPromises, - reason_2; - return tslib_1.__generator(this, function (_e) { - switch (_e.label) { - case 0: - ciDir = env_1.getCiFolder(); - distDir = path.resolve(ciDir, 'dist'); - distContentDir = path.resolve(distDir, getPluginId_1.getPluginId()); - pluginJsonFile = path.resolve(distContentDir, 'plugin.json'); - pluginInfo = pluginValidation_1.getPluginJson(pluginJsonFile).info; - PUBLISH_DIR = path.resolve(env_1.getCiFolder(), 'packages'); - commitHash = this.commitHash || ((_a = pluginInfo.build) === null || _a === void 0 ? void 0 : _a.hash); - _e.label = 1; - case 1: - _e.trys.push([1, 5, , 6]); - return [4 /*yield*/, this.git.client.get('releases/tags/v' + pluginInfo.version)]; - case 2: - latestRelease = _e.sent(); - if (!(latestRelease.data.tag_name === 'v' + pluginInfo.version)) { - return [3 /*break*/, 4]; - } - return [4 /*yield*/, this.git.client.delete('releases/' + latestRelease.data.id)]; - case 3: - _e.sent(); - _e.label = 4; - case 4: - return [3 /*break*/, 6]; - case 5: - reason_1 = _e.sent(); - if (reason_1.response.status !== 404) { - // 404 just means no release found. Not an error. Anything else though, re throw the error - throw reason_1; - } - return [3 /*break*/, 6]; - case 6: - _e.trys.push([6, 9, , 10]); - return [ - 4 /*yield*/, - this.git.client.post('releases', { - tag_name: 'v' + pluginInfo.version, - target_commitish: commitHash, - name: 'v' + pluginInfo.version, - body: this.releaseNotes, - draft: false, - prerelease: false, - }), - ]; - case 7: - newReleaseResponse = _e.sent(); - publishPromises = this.publishAssets( - PUBLISH_DIR, - 'https://uploads.github.com/repos/' + - this.username + - '/' + - this.repository + - '/releases/' + - newReleaseResponse.data.id + - '/assets' - ); - return [4 /*yield*/, Promise.all(publishPromises)]; - case 8: - _e.sent(); - return [3 /*break*/, 10]; - case 9: - reason_2 = _e.sent(); - console.log(reason_2); - // Rethrow the error so that we can trigger a non-zero exit code to circle-ci - throw reason_2; - case 10: - return [2 /*return*/]; - } - }); - }); - }; - return GitHubRelease; -})(); -exports.GitHubRelease = GitHubRelease; -//# sourceMappingURL=githubRelease.js.map7027e10521e9 diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/scripts/deploy-common.sh b/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/scripts/deploy-common.sh deleted file mode 100755 index 9263691386f..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/scripts/deploy-common.sh +++ /dev/null @@ -1,52 +0,0 @@ -#!/bin/sh - -## -# Script to deploy a docker image. Must return exit code 0 -# -do_exit() { - message="$1" - exit_code="$2" - - echo "$message" - exit $exit_code -} - - -## -# Get file, get's a file, validates the SHA -# @param filename -# @param expected sha value -# @returns 0 if successful, -1 of checksum validation failed. -# -get_file () { - [ -n "$1" ] && url=$1 || do_exit "url required" 1 - [ -n "$2" ] && dest=$2 || do_exit "destination required" 2 - sha=$3 - file=$(basename $dest) - - curl -fL "${url}" -o "$dest" - if [ -n "$sha" ]; then - echo "$sha $dest" | sha256sum || do_exit "Checksum validation failed for $file. Exiting" 1 - fi -} - -untar_file () { - [ -n "$1" ] && src=$1 || do_exit "src required" 1 - [ -n "$2" ] && dest=$2 || dest="/usr/local" - - tar -C "$dest" -xf "$src" && /bin/rm -rf "$src" -} - -## -# WIP: Just started this and not finished. -# The intent it to download a release from a git repo, -# compile, and install -get_latest_release () { - tarsrc=$(curl -sL "https://api.github.com/repos/$1/$2/releases/latest" | jq ".tarball_url" | tr -d '"') - curl -fL -o /tmp/autoretrieved.tar.gz "$tarsrc" - origdir=$PWD - reponame=$(tar zxvf autoretrieved.tar.gz | tail -1 | awk -F / '{print $1}') - cd "/tmp/$reponame" - #perform compile - cd $origdir -} diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/scripts/deploy-user.sh b/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/scripts/deploy-user.sh deleted file mode 100755 index f8926ec4678..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/scripts/deploy-user.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -source "./deploy-common.sh" - diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/scripts/deploy.sh b/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/scripts/deploy.sh deleted file mode 100755 index 94345950078..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/scripts/deploy.sh +++ /dev/null @@ -1,80 +0,0 @@ -#!/bin/sh -set -eo pipefail - -source "./deploy-common.sh" - -# Make libgcc compatible -mkdir /lib64 && ln -s /lib/libc.musl-x86_64.so.1 /lib64/ld-linux-x86-64.so.2 - -# Replace cp with something that mocks the one that ci-package needs -rm /bin/cp -mv /usr/local/bin/cp /bin/cp - -apk add --no-cache curl npm yarn build-base openssh git-lfs perl-utils coreutils python3 - -# -# Only relevant for testing, but cypress does not work with musl/alpine. -# -# apk add --no-cache xvfb glib nss nspr gdk-pixbuf "gtk+3.0" pango atk cairo dbus-libs libxcomposite libxrender libxi libxtst libxrandr libxscrnsaver alsa-lib at-spi2-atk at-spi2-core cups-libs gcompat libc6-compat - -# Install Go -filename="go1.19.4.linux-amd64.tar.gz" -get_file "https://dl.google.com/go/$filename" "/tmp/$filename" "c9c08f783325c4cf840a94333159cc937f05f75d36a8b307951d5bd959cf2ab8" -untar_file "/tmp/$filename" - -# Install golangci-lint -GOLANGCILINT_VERSION=1.50.0 -filename="golangci-lint-${GOLANGCILINT_VERSION}-linux-amd64" -get_file "https://github.com/golangci/golangci-lint/releases/download/v${GOLANGCILINT_VERSION}/$filename.tar.gz" \ - "/tmp/$filename.tar.gz" \ - "b4b329efcd913082c87d0e9606711ecb57415b5e6ddf233fde9e76c69d9b4e8b" -untar_file "/tmp/$filename.tar.gz" -ln -s /usr/local/${filename}/golangci-lint /usr/local/bin/golangci-lint -ln -s /usr/local/go/bin/go /usr/local/bin/go -ln -s /usr/local/go/bin/gofmt /usr/local/bin/gofmt -chmod 755 /usr/local/bin/golangci-lint - -# Install dependencies -apk add --no-cache fontconfig zip jq - -# Install code climate -get_file "https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64" \ - "/usr/local/bin/cc-test-reporter" \ - "20d1d4e2b399d0287d91e65faeee8ffbef08e3262b0be5eda7def7b3c2799ddd" -chmod 755 /usr/local/bin/cc-test-reporter - -curl -fL -o /usr/local/bin/grabpl "https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.27/grabpl" - -apk add --no-cache git -# Install Mage -mkdir -pv /tmp/mage $HOME/go/bin -git clone https://github.com/magefile/mage.git /tmp/mage -cd /tmp/mage && go run bootstrap.go -mv $HOME/go/bin/mage /usr/local/bin - -wget -O - -q https://raw.githubusercontent.com/securego/gosec/master/install.sh | sh -s -- -b /usr/local/bin v2.14.0 - -source "/etc/profile" -sh -l -c "go get -u github.com/mgechev/revive" -for file in $(ls $HOME/go/bin); do - mv -v $HOME/go/bin/$file /usr/local/bin/$file -done - -# Install grafana-toolkit deps -current_dir=$PWD -cd /usr/local/grafana-toolkit && yarn install && cd $current_dir -ln -s /usr/local/grafana-toolkit/bin/grafana-toolkit.js /usr/local/bin/grafana-toolkit - -GOOGLE_SDK_VERSION=365.0.1 -GOOGLE_SDK_CHECKSUM=17003cdba67a868c2518ac16efa60dc6175533b7a9fb87304459784308e30fb0 - -curl -fLO https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-sdk-${GOOGLE_SDK_VERSION}-linux-x86_64.tar.gz -echo "${GOOGLE_SDK_CHECKSUM} google-cloud-sdk-${GOOGLE_SDK_VERSION}-linux-x86_64.tar.gz" | sha256sum --check --status -tar xvzf google-cloud-sdk-${GOOGLE_SDK_VERSION}-linux-x86_64.tar.gz -C /opt -rm google-cloud-sdk-${GOOGLE_SDK_VERSION}-linux-x86_64.tar.gz -ln -s /opt/google-cloud-sdk/bin/gsutil /usr/bin/gsutil -ln -s /opt/google-cloud-sdk/bin/gcloud /usr/bin/gcloud - -# Cleanup after yourself -/bin/rm -rf /tmp/mage -/bin/rm -rf $HOME/go diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/test/docker-compose.yml b/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/test/docker-compose.yml deleted file mode 100644 index 9a3e53f2cc3..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/test/docker-compose.yml +++ /dev/null @@ -1,18 +0,0 @@ -version: '3' -services: - citest: - image: "amd64/alpine" - user: root - volumes: - - ../scripts:/home/circleci/scripts - - ../install:/home/circleci/install - - ${HOME}/.ssh:/root/.ssh - - ../../..:/home/circleci/grafana-toolkit - cibuilt: - image: "grafana/grafana-plugin-ci:latest-alpine" - user: root - volumes: - - ../scripts:/home/circleci/scripts - - ../install:/home/circleci/install - - ${HOME}/.ssh:/root/.ssh - - ../../..:/home/circleci/grafana-toolkit diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/test/start.sh b/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/test/start.sh deleted file mode 100755 index f5551eb3b6e..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci-alpine/test/start.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -function finish { - echo "Exiting and cleaning up docker image" - docker-compose down -} -trap finish EXIT - -# Enter the docker container -if [ "$1" = "built" ]; then - docker-compose run cibuilt sh -c "cd /home/circleci; exec sh --login -i" -else - docker-compose run citest sh -c "cd /home/circleci; exec sh --login -i" -fi diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/Dockerfile b/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/Dockerfile deleted file mode 100644 index 6aa2682c27b..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM debian:buster-slim - -ENV DEBIAN_FRONTEND=noninteractive - -COPY scripts scripts -COPY install /usr/local - -RUN cd scripts && ./deploy.sh -ENV DEBIAN_FRONTEND=newt diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/README.md b/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/README.md deleted file mode 100644 index 1b37557a0b3..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# Using this docker image - -## User - -The user will be `circleci` -The home directory will be `/home/circleci` - -## Node - -- node 14 is installed -- yarn is installed globally -- npm is installed globally - -## Go - -- Go is installed in `/usr/local/bin/go` -- golangci-lint is installed in `/usr/local/bin/golangci-lint` -- mage is installed in `/usr/local/bin/mage` - -All of the above directories are in the path, so there is no need to specify fully qualified paths. - -## Grafana - -- Installed in `/home/circleci/src/grafana` -- `yarn install` has been run - -## Integration/Release Testing - -There are 4 previous versions pre-downloaded to /usr/local/grafana. These versions are: - -1. 6.6.2 -2. 6.5.3 -3. 6.4.5 -4. 6.3.7 - -To test, your CircleCI config will need a run section with something similar to the following - -``` -- run: - name: Setup Grafana (local install) - command: | - sudo dpkg -i /usr/local/grafana/deb/grafana_6.6.2_amd64.deb - sudo cp ci/grafana-test-env/custom.ini /usr/share/grafana/conf/custom.ini - sudo cp ci/grafana-test-env/custom.ini /etc/grafana/grafana.ini - sudo service grafana-server start - grafana-cli --version -``` - -# Building - -To build, cd to `/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e` - -``` -./build.sh -``` - -# Developing/Testing - -To test, you should have docker-compose installed. - -``` -cd test -./start.sh -``` - -You will be in /home/circleci/test with the buildscripts installed to the local directory. -Do your edits/run tests. When saving, your edits will be available in the container immediately. diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/build.sh b/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/build.sh deleted file mode 100755 index 14ca02a1dc9..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/build.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash -set -eo pipefail - -source ./common.sh - -# -# No longer required, but useful to keep just in case we want to deploy -# changes in toolkit directly to the docker image -# -if [ -n "$INCLUDE_TOOLKIT" ]; then - /bin/rm -rfv install/grafana-toolkit - mkdir -pv install/grafana-toolkit - cp -rv ../../bin install/grafana-toolkit - cp -rv ../../src install/grafana-toolkit - cp -v ../../package.json install/grafana-toolkit - cp -v ../../tsconfig.json install/grafana-toolkit -fi - -docker build -t ${DOCKER_IMAGE_NAME} . -docker push $DOCKER_IMAGE_NAME -docker tag ${DOCKER_IMAGE_NAME} ${DOCKER_IMAGE_BASE_NAME}:latest -docker push ${DOCKER_IMAGE_BASE_NAME}:latest - -[ -n "$INCLUDE_TOOLKIT" ] && /bin/rm -rfv install/grafana-toolkit diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/common.sh b/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/common.sh deleted file mode 100755 index 29714cd5a84..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/common.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -## -## Common variable declarations -## Find the latest tag on https://hub.docker.com/r/grafana/grafana-plugin-ci-e2e/tags -## - -DOCKER_IMAGE_BASE_NAME="grafana/grafana-plugin-ci-e2e" -DOCKER_IMAGE_VERSION="1.6.1" -DOCKER_IMAGE_NAME="${DOCKER_IMAGE_BASE_NAME}:${DOCKER_IMAGE_VERSION}" diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/install/ginstall b/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/install/ginstall deleted file mode 100755 index a71019bdbef..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/install/ginstall +++ /dev/null @@ -1,73 +0,0 @@ -#!/bin/bash -## -# gget -# A script to get and install grafana versions -# for usage information see "show_help" below. -# - -latest=$(wget -O - 'https://raw.githubusercontent.com/grafana/grafana/main/latest.json' | jq -r '.stable') -canary=$(wget -O - "https://grafana.com/api/grafana-enterprise/versions" | jq ".items[0].version" | tr -d '"') - -show_help() { - echo "Usage: gget " - echo "" - echo "where can be:" - echo " 1) A version from https://grafana.com/grafana/download (ex x.y.z)" - echo " 2) latest (currently $latest)" - echo " 3) canary (currently $canary)" - echo "" - echo " -h, --help: Display this help message" - echo "" - exit 0 -} - -opts=$(getopt -o h --long help -n 'gget' -- "$@") -[ $? -eq 0 ] || { - show_help -} - -eval set -- "$opts" -while true; do - case "$1" in - -h | --help) - show_help - ;; - --) - shift - break - ;; - *) - break - ;; - esac - shift -done - -[ -z "$1" ] && show_help - -# Make sure the script is being run as root -if [ $EUID -ne 0 ]; then - echo "This script must be run as root" - exit 1 -fi - -## -# MAIN -# -# Enough setup, let's actually do something -# -version=$1 -if [ "$version" == "latest" ]; then - version="$latest" - wget -O - "https://dl.grafana.com/enterprise/release/grafana-enterprise-${version}.linux-amd64.tar.gz" | tar -C /opt -zxf - -elif [ "$version" == "canary" ]; then - version="$canary" - wget -O - "https://dl.grafana.com/enterprise/main/grafana-enterprise-${version}.linux-amd64.tar.gz" | tar -C /opt -zxf - -else - wget -O - "https://dl.grafana.com/enterprise/release/grafana-enterprise-${version}.linux-amd64.tar.gz" | tar -C /opt -zxf - -fi - -/bin/rm -rf /opt/grafana > /dev/null 2>&1 || true -ln -s /opt/grafana-${version} /opt/grafana - -# nohup /opt/grafana/bin/grafana-server -config /opt/grafana/conf/defaults.ini -homepath /opt/grafana >/dev/null 2>&1 & diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/scripts/deploy-common.sh b/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/scripts/deploy-common.sh deleted file mode 100755 index af585d2eb19..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/scripts/deploy-common.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/bash - -## -# Script to deploy a docker image. Must return exit code 0 -# -do_exit() { - message="$1" - exit_code="$2" - - echo "$message" - exit $exit_code -} - -## -# Get file, get's a file, validates the SHA -# @param filename -# @param expected sha value -# @returns 0 if successful, -1 of checksum validation failed. -# -get_file () { - [ -n "$1" ] && url=$1 || do_exit "url required" -1 - [ -n "$2" ] && dest=$2 || do_exit "destination required" -2 - sha=$3 - file=$(basename $dest) - - wget "$url" -O "$dest" - if [ -n "$sha" ]; then - echo "$sha $dest" | sha256sum --check --status || do_exit "Checksum validation failed for $file. Exiting" -1 - fi -} - -untar_file () { - [ -n "$1" ] && src=$1 || do_exit "src required" -1 - [ -n "$2" ] && dest=$2 || dest="/usr/local" - - tar -C "$dest" -xf "$src" && /bin/rm -rf "$src" -} diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/scripts/deploy-slim.sh b/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/scripts/deploy-slim.sh deleted file mode 100755 index 9c3c0a31e2d..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/scripts/deploy-slim.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash -source "/etc/profile" - -apt-get --allow-insecure-repositories update -apt-get install --allow-unauthenticated -y \ - build-essential \ - wget git sudo adduser \ - libfontconfig1 \ - locate \ - libnss3 libnspr4 \ - libgdk-pixbuf2.0-0 \ - libgtk-3-0 \ - libpangocairo-1.0-0 \ - libpango-1.0-0 \ - libatk1.0-0 \ - libcairo2 \ - libdbus-1-3 \ - libxcomposite1 libxrender1 libxcursor1 libxi6 libxtst6 libxrandr2 libxss1 xauth xvfb \ - libasound2 \ - libatk-bridge2.0-0 \ - libatspi2.0-0 \ - libcups2 \ - jq net-tools git-lfs unzip pkg-config zip \ - libaio1 libaio-dev \ - netcat \ - libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb - diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/scripts/deploy-user.sh b/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/scripts/deploy-user.sh deleted file mode 100755 index 08f2b7b950a..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/scripts/deploy-user.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -source "./deploy-common.sh" - diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/scripts/deploy.sh b/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/scripts/deploy.sh deleted file mode 100755 index 99e711aee87..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/scripts/deploy.sh +++ /dev/null @@ -1,65 +0,0 @@ -#!/bin/bash -set -eo pipefail - -source "/etc/profile" -source "./deploy-slim.sh" -source "./deploy-common.sh" - -NODEVER="v16.13.2" -# Install Node -wget -O - "https://nodejs.org/dist/${NODEVER}/node-${NODEVER}-linux-x64.tar.xz" | tar Jvxf - -C "/tmp" - -# Move node to /usr/local so it's in the path -pushd /tmp/node-${NODEVER}-linux-x64 -/bin/rm -f CHANGELOG.md README.md LICENSE -/bin/cp -r * /usr/local -popd -/bin/rm -rf /tmp/node-${NODEVER} - -# Resource the profile so we know our path is being honoured -source "/etc/profile" -# Install Yarn. Not in the path yet so fully qualified -npm i -g yarn - -# Install Go -filename="go1.19.4.linux-amd64.tar.gz" -get_file "https://dl.google.com/go/$filename" "/tmp/$filename" "c9c08f783325c4cf840a94333159cc937f05f75d36a8b307951d5bd959cf2ab8" -untar_file "/tmp/$filename" - -# Install golangci-lint -GOLANGCILINT_VERSION=1.50.0 -filename="golangci-lint-${GOLANGCILINT_VERSION}-linux-amd64" -get_file "https://github.com/golangci/golangci-lint/releases/download/v${GOLANGCILINT_VERSION}/$filename.tar.gz" \ - "/tmp/$filename.tar.gz" \ - "b4b329efcd913082c87d0e9606711ecb57415b5e6ddf233fde9e76c69d9b4e8b" -untar_file "/tmp/$filename.tar.gz" -ln -s /usr/local/${filename}/golangci-lint /usr/local/bin/golangci-lint -ln -s /usr/local/go/bin/go /usr/local/bin/go -ln -s /usr/local/go/bin/gofmt /usr/local/bin/gofmt -chmod 755 /usr/local/bin/golangci-lint - -# Install code climate -get_file "https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64" \ - "/usr/local/bin/cc-test-reporter" \ - "20d1d4e2b399d0287d91e65faeee8ffbef08e3262b0be5eda7def7b3c2799ddd" -chmod 755 /usr/local/bin/cc-test-reporter - -wget -O /usr/local/bin/grabpl "https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.27/grabpl" -chmod +x /usr/local/bin/grabpl - -# Install Mage -mkdir -pv /tmp/mage $HOME/go/bin -git clone https://github.com/magefile/mage.git /tmp/mage -pushd /tmp/mage && go run bootstrap.go && popd -mv $HOME/go/bin/mage /usr/local/bin -# Cleanup after yourself -/bin/rm -rf /tmp/mage -/bin/rm -rf $HOME/go - -# add cypress -yarn global add cypress -# verify cypress install -cypress verify - -# Get the size down -/bin/rm -rf /var/lib/apt/lists diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/test/docker-compose.yml b/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/test/docker-compose.yml deleted file mode 100644 index 5d6297c4833..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/test/docker-compose.yml +++ /dev/null @@ -1,18 +0,0 @@ -version: '3' -services: - citest: - image: "debian:buster-slim" - user: root - volumes: - - ../scripts:/root/scripts - - ../install:/root/install - - ${HOME}/.ssh:/root/.ssh - - ../../..:/root/grafana-toolkit - cibuilt: - image: "grafana/grafana-plugin-ci-e2e" - user: root - volumes: - - ../scripts:/root/scripts - - ../install:/root/install - - ${HOME}/.ssh:/root/.ssh - - ../../..:/root/grafana-toolkit diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/test/start.sh b/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/test/start.sh deleted file mode 100755 index 2552c964a85..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci-e2e/test/start.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -function finish { - echo "Exiting and cleaning up docker image" - docker-compose down -} -trap finish EXIT - -# Enter the docker container -if [ "$1" = "built" ]; then - docker-compose run cibuilt sh -c "cd /root; exec bash --login -i" -else - docker-compose run citest sh -c "cd /root; exec bash --login -i" -fi diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci/Dockerfile b/packages/grafana-toolkit/docker/grafana-plugin-ci/Dockerfile deleted file mode 100644 index 46228193273..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci/Dockerfile +++ /dev/null @@ -1,8 +0,0 @@ -FROM debian:testing-20210111-slim -USER root -COPY scripts scripts -WORKDIR scripts -RUN apt-get update && \ - apt-get install -y wget && \ - ./deploy.sh -COPY install/gget /usr/local/bin/gget diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci/README.md b/packages/grafana-toolkit/docker/grafana-plugin-ci/README.md deleted file mode 100644 index 18bc1aec592..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci/README.md +++ /dev/null @@ -1,71 +0,0 @@ -# Using this docker image - -Currently tagged and uploaded to dockerhub as srclosson/integrations-ci-build - -Based off of `circleci/node:12-browsers` - -## User - -The user will be `circleci` -The home directory will be `/home/circleci` - -## Node - -- node 12 is installed -- yarn is installed globally -- npm is installed globally - -## Go - -- Go is installed in `/usr/local/bin/go` -- golangci-lint is installed in `/usr/local/bin/golangci-lint` -- mage is installed in `/home/circleci/go/bin/mage` - -All of the above directories are in the path, so there is no need to specify fully qualified paths. - -## Grafana - -- Installed in `/home/circleci/src/grafana` -- `yarn install` has been run - -## Integration/Release Testing - -There are 4 previous versions pre-downloaded to /usr/local/grafana. These versions are: - -1. 6.6.2 -2. 6.5.3 -3. 6.4.5 -4. 6.3.7 - -To test, your CircleCI config will need a run section with something similar to the following - -``` -- run: - name: Setup Grafana (local install) - command: | - sudo dpkg -i /usr/local/grafana/deb/grafana_6.6.2_amd64.deb - sudo cp ci/grafana-test-env/custom.ini /usr/share/grafana/conf/custom.ini - sudo cp ci/grafana-test-env/custom.ini /etc/grafana/grafana.ini - sudo service grafana-server start - grafana-cli --version -``` - -# Building - -To build, cd to `/packages/grafana-toolkit/docker/grafana-plugin-ci` - -``` -./build.sh -``` - -# Developing/Testing - -To test, you should have docker-compose installed. - -``` -cd test -./start.sh -``` - -You will be in /home/circleci/test with the buildscripts installed to the local directory. -Do your edits/run tests. When saving, your edits will be available in the container immediately. diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci/build.sh b/packages/grafana-toolkit/docker/grafana-plugin-ci/build.sh deleted file mode 100755 index 3d8d3d7110a..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci/build.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash -set -eo pipefail - -source ./common.sh - -docker build -t ${DOCKER_IMAGE_NAME} . -docker push $DOCKER_IMAGE_NAME - diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci/common.sh b/packages/grafana-toolkit/docker/grafana-plugin-ci/common.sh deleted file mode 100755 index 47d2b068d8d..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci/common.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -## -## Common variable declarations -## Find the latest tags on https://hub.docker.com/r/grafana/grafana-plugin-ci/tags -## - -DOCKER_IMAGE_NAME="grafana/grafana-plugin-ci:1.6.1" diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci/install/gget b/packages/grafana-toolkit/docker/grafana-plugin-ci/install/gget deleted file mode 100755 index 3e38e2d5911..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci/install/gget +++ /dev/null @@ -1,63 +0,0 @@ -#!/bin/bash -## -# gget -# A script to get and install grafana versions -# for usage information see "show_help" below. -# - -latest=$(curl -s 'https://raw.githubusercontent.com/grafana/grafana/main/latest.json' | jq -r '.stable') -canary=$(curl -s "https://grafana.com/api/grafana/versions" | jq ".items[0].version" | tr -d '"') - -show_help() { - echo "Usage: gget " - echo "" - echo "where can be:" - echo " 1) A version from https://grafana.com/grafana/download (ex x.y.z)" - echo " 2) latest (currently $latest)" - echo " 3) canary (currently $canary)" - echo "" - echo " -h, --help: Display this help message" - echo "" - exit 0 -} - -opts=$(getopt -o h --long help -n 'gget' -- "$@") -[ $? -eq 0 ] || { - show_help -} - -eval set -- "$opts" -while true; do - case "$1" in - -h | --help) - show_help - ;; - --) - shift - break - ;; - *) - break - ;; - esac - shift -done - -[ -z "$1" ] && show_help - -# Make sure the script is being run as root -if [ $EUID -ne 0 ]; then - echo "This script must be run as root" - exit 1 -fi - -## -# MAIN -# -# Enough setup, let's actually do something -# -version=$1 -[ "$version" == "latest" ] && version="$latest" -[ "$version" == "canary" ] && version="$canary" -wget "https://dl.grafana.com/oss/release/grafana_${version}_amd64.deb" -O "/tmp/grafana_${version}_amd64.deb" -dpkg -i "/tmp/grafana_${version}_amd64.deb" && /bin/rm -rfv "/tmp/grafana_${version}_amd64.deb" diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci/scripts/deploy-common.sh b/packages/grafana-toolkit/docker/grafana-plugin-ci/scripts/deploy-common.sh deleted file mode 100755 index 524bb5e4ec6..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci/scripts/deploy-common.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/bash - -## -# Script to deploy a docker image. Must return exit code 0 -# -do_exit() { - message="$1" - exit_code="$2" - - echo "$message" - exit $exit_code -} - - -## -# Get file, get's a file, validates the SHA -# @param filename -# @param expected sha value -# @returns 0 if successful, -1 of checksum validation failed. -# -get_file () { - [ -n "$1" ] && url=$1 || do_exit "url required" -1 - [ -n "$2" ] && dest=$2 || do_exit "destination required" -2 - sha=$3 - file=$(basename $dest) - - wget "$url" -O "$dest" - if [ -n "$sha" ]; then - echo "$sha $dest" | sha256sum --check --status || do_exit "Checksum validation failed for $file. Exiting" -1 - fi -} - -untar_file () { - [ -n "$1" ] && src=$1 || do_exit "src required" -1 - [ -n "$2" ] && dest=$2 || dest="/usr/local" - - tar -C "$dest" -xf "$src" && /bin/rm -rf "$src" -} \ No newline at end of file diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci/scripts/deploy-user.sh b/packages/grafana-toolkit/docker/grafana-plugin-ci/scripts/deploy-user.sh deleted file mode 100755 index 08f2b7b950a..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci/scripts/deploy-user.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -source "./deploy-common.sh" - diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci/scripts/deploy.sh b/packages/grafana-toolkit/docker/grafana-plugin-ci/scripts/deploy.sh deleted file mode 100755 index 3ee0bff1e16..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci/scripts/deploy.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/bin/bash -source "./deploy-common.sh" - -# Install Go -filename="go1.19.4.linux-amd64.tar.gz" -get_file "https://dl.google.com/go/$filename" "/tmp/$filename" "c9c08f783325c4cf840a94333159cc937f05f75d36a8b307951d5bd959cf2ab8" -untar_file "/tmp/$filename" - -# Install golangci-lint -GOLANGCILINT_VERSION=1.50.0 -filename="golangci-lint-${GOLANGCILINT_VERSION}-linux-amd64" -get_file "https://github.com/golangci/golangci-lint/releases/download/v${GOLANGCILINT_VERSION}/$filename.tar.gz" \ - "/tmp/$filename.tar.gz" \ - "b4b329efcd913082c87d0e9606711ecb57415b5e6ddf233fde9e76c69d9b4e8b" -untar_file "/tmp/$filename.tar.gz" -ln -s /usr/local/${filename}/golangci-lint /usr/local/bin/golangci-lint -ln -s /usr/local/go/bin/go /usr/local/bin/go -ln -s /usr/local/go/bin/gofmt /usr/local/bin/gofmt -chmod 755 /usr/local/bin/golangci-lint - -# Install dependencies -apt-get update -y && apt-get install -y adduser libfontconfig1 locate && /bin/rm -rf /var/lib/apt/lists/* - -# Install code climate -get_file "https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64" \ - "/usr/local/bin/cc-test-reporter" \ - "20d1d4e2b399d0287d91e65faeee8ffbef08e3262b0be5eda7def7b3c2799ddd" -chmod 755 /usr/local/bin/cc-test-reporter - -wget -O /usr/local/bin/grabpl "https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.27/grabpl" -chmod +x /usr/local/bin/grabpl - -# Install Mage -mkdir -pv /tmp/mage $HOME/go/bin -git clone https://github.com/magefile/mage.git /tmp/mage -pushd /tmp/mage && go run bootstrap.go && popd -mv $HOME/go/bin/mage /usr/local/bin - -GOOGLE_SDK_VERSION=365.0.1 -GOOGLE_SDK_CHECKSUM=17003cdba67a868c2518ac16efa60dc6175533b7a9fb87304459784308e30fb0 - -curl -fLO https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-sdk-${GOOGLE_SDK_VERSION}-linux-x86_64.tar.gz -echo "${GOOGLE_SDK_CHECKSUM} google-cloud-sdk-${GOOGLE_SDK_VERSION}-linux-x86_64.tar.gz" | sha256sum --check --status -tar xvzf google-cloud-sdk-${GOOGLE_SDK_VERSION}-linux-x86_64.tar.gz -C /opt -rm google-cloud-sdk-${GOOGLE_SDK_VERSION}-linux-x86_64.tar.gz -ln -s /opt/google-cloud-sdk/bin/gsutil /usr/bin/gsutil -ln -s /opt/google-cloud-sdk/bin/gcloud /usr/bin/gcloud - -# Cleanup after yourself -/bin/rm -rf /tmp/mage -/bin/rm -rf $HOME/go - -# Perform user specific initialization -sudo -u circleci ./deploy-user.sh - -# Get the size down -/bin/rm -rf /var/lib/apt/lists diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci/test/docker-compose.yml b/packages/grafana-toolkit/docker/grafana-plugin-ci/test/docker-compose.yml deleted file mode 100644 index d734b3a38e6..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci/test/docker-compose.yml +++ /dev/null @@ -1,10 +0,0 @@ -version: '3' -services: - citest: - image: "circleci/node:12-browsers" - user: root - volumes: - - ../scripts:/home/circleci/scripts - - ../install:/home/circleci/install - - ${HOME}/.ssh:/root/.ssh - - ../../..:/home/circleci/grafana-toolkit diff --git a/packages/grafana-toolkit/docker/grafana-plugin-ci/test/start.sh b/packages/grafana-toolkit/docker/grafana-plugin-ci/test/start.sh deleted file mode 100755 index af3d91cb4a0..00000000000 --- a/packages/grafana-toolkit/docker/grafana-plugin-ci/test/start.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -function finish { - echo "Exiting and cleaning up docker image" - docker-compose down -} -trap finish EXIT - -# Enter the docker container -docker-compose run citest bash -c "cd /home/circleci; exec bash --login -i" diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index ab5bc4838b5..1524aee83d0 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -81,16 +81,16 @@ "ol": "7.1.0", "prismjs": "1.29.0", "rc-cascader": "3.8.0", - "rc-drawer": "6.0.1", - "rc-slider": "10.0.1", + "rc-drawer": "6.1.2", + "rc-slider": "10.1.0", "rc-time-picker": "^3.7.3", - "rc-tooltip": "5.2.2", + "rc-tooltip": "5.3.1", "react-beautiful-dnd": "13.1.1", "react-calendar": "3.9.0", "react-colorful": "5.6.1", "react-custom-scrollbars-2": "4.5.0", "react-dropzone": "14.2.3", - "react-highlight-words": "0.18.0", + "react-highlight-words": "0.20.0", "react-hook-form": "7.5.3", "react-i18next": "^12.0.0", "react-inlinesvg": "3.0.1", @@ -109,7 +109,7 @@ "slate-react": "0.22.10", "tinycolor2": "1.4.2", "tslib": "2.4.1", - "uplot": "1.6.23", + "uplot": "1.6.24", "uuid": "9.0.0" }, "devDependencies": { @@ -133,7 +133,7 @@ "@storybook/preset-scss": "1.0.3", "@storybook/react": "6.5.14", "@storybook/theming": "6.5.14", - "@testing-library/dom": "8.19.0", + "@testing-library/dom": "8.20.0", "@testing-library/jest-dom": "5.16.5", "@testing-library/react": "12.1.4", "@testing-library/react-hooks": "8.0.1", diff --git a/packages/grafana-ui/src/components/ContextMenu/ContextMenu.tsx b/packages/grafana-ui/src/components/ContextMenu/ContextMenu.tsx index bde5b951c51..04bf348225e 100644 --- a/packages/grafana-ui/src/components/ContextMenu/ContextMenu.tsx +++ b/packages/grafana-ui/src/components/ContextMenu/ContextMenu.tsx @@ -33,7 +33,7 @@ export const ContextMenu: React.FC = React.memo( const OFFSET = 5; const collisions = { right: window.innerWidth < x + rect.width, - bottom: window.innerHeight < rect.bottom + rect.height + OFFSET, + bottom: window.innerHeight < y + rect.height + OFFSET, }; setPositionStyles({ diff --git a/packages/grafana-ui/src/components/FileDropzone/FileDropzone.test.tsx b/packages/grafana-ui/src/components/FileDropzone/FileDropzone.test.tsx index e60f6818e2c..42ac0652b90 100644 --- a/packages/grafana-ui/src/components/FileDropzone/FileDropzone.test.tsx +++ b/packages/grafana-ui/src/components/FileDropzone/FileDropzone.test.tsx @@ -33,6 +33,14 @@ describe('The FileDropzone component', () => { expect(screen.getByText('Accepted file type: .json')).toBeInTheDocument(); }); + it('should show an error message when the file size exceeds the max file size', async () => { + render(); + + dispatchEvt(screen.getByTestId('dropzone'), 'drop', mockData(files)); + + expect(await screen.findByText('File is larger than 1 B')).toBeInTheDocument(); + }); + it('should show the accepted file type(s) when passed in as a array of strings', () => { render(); diff --git a/packages/grafana-ui/src/components/FileDropzone/FileDropzone.tsx b/packages/grafana-ui/src/components/FileDropzone/FileDropzone.tsx index 81961749565..228c013282f 100644 --- a/packages/grafana-ui/src/components/FileDropzone/FileDropzone.tsx +++ b/packages/grafana-ui/src/components/FileDropzone/FileDropzone.tsx @@ -1,9 +1,9 @@ import { css, cx } from '@emotion/css'; import { isString, uniqueId } from 'lodash'; import React, { ReactNode, useCallback, useState } from 'react'; -import { Accept, DropEvent, DropzoneOptions, FileRejection, useDropzone } from 'react-dropzone'; +import { Accept, DropEvent, DropzoneOptions, FileError, FileRejection, useDropzone, ErrorCode } from 'react-dropzone'; -import { GrafanaTheme2 } from '@grafana/data'; +import { formattedValueToString, getValueFormat, GrafanaTheme2 } from '@grafana/data'; import { useTheme2 } from '../../themes'; import { Alert } from '../Alert/Alert'; @@ -59,7 +59,9 @@ export interface DropzoneFile { export function FileDropzone({ options, children, readAs, onLoad, fileListRenderer, onFileRemove }: FileDropzoneProps) { const [files, setFiles] = useState([]); - const [errorMessages, setErrorMessages] = useState([]); + const [fileErrors, setErrorMessages] = useState([]); + + const formattedSize = getValueFormat('decbytes')(options?.maxSize ? options?.maxSize : 0); const setFileProperty = useCallback( (customFile: DropzoneFile, action: (customFileToModify: DropzoneFile) => void) => { @@ -166,11 +168,15 @@ export function FileDropzone({ options, children, readAs, onLoad, fileListRender }); const setErrors = (rejectedFiles: FileRejection[]) => { - let errors: string[] = []; + let errors: FileError[] = []; rejectedFiles.map((rejectedFile) => { - rejectedFile.errors.map((error) => { - if (errors.indexOf(error.message) === -1) { - errors.push(error.message); + rejectedFile.errors.map((newError) => { + if ( + errors.findIndex((presentError) => { + return presentError.code === newError.code && presentError.message === newError.message; + }) === -1 + ) { + errors.push(newError); } }); }); @@ -178,12 +184,21 @@ export function FileDropzone({ options, children, readAs, onLoad, fileListRender setErrorMessages(errors); }; - const getErrorMessages = () => { + const renderErrorMessages = (errors: FileError[]) => { return (
- {errorMessages.map((error) => { - return
{error}
; + {errors.map((error) => { + switch (error.code) { + case ErrorCode.FileTooLarge: + return ( +
+ File is larger than {formattedValueToString(formattedSize)} +
+ ); + default: + return
{error.message}
; + } })}
@@ -200,10 +215,19 @@ export function FileDropzone({ options, children, readAs, onLoad, fileListRender {children ?? } - {errorMessages.length > 0 && getErrorMessages()} - {options?.accept && ( - {getAcceptedFileTypeText(options.accept)} - )} + {fileErrors.length > 0 && renderErrorMessages(fileErrors)} +
+ {options?.accept && ( + + {getAcceptedFileTypeText(options.accept)} + + )} + {options?.maxSize && ( + {`Max file size: ${formattedValueToString( + formattedSize + )}`} + )} +
{fileList} ); @@ -252,6 +276,7 @@ export function FileDropzoneDefaultChildren({ ); } + function getPrimaryText(files: DropzoneFile[], options?: BackwardsCompatibleDropzoneOptions) { if (options?.multiple === undefined || options?.multiple) { return 'Upload file'; @@ -304,6 +329,12 @@ function getStyles(theme: GrafanaTheme2, isDragActive?: boolean) { flex-direction: column; align-items: center; `, + acceptContainer: css` + display: flex; + `, + acceptedFiles: css` + flex-grow: 1; + `, acceptMargin: css` margin: ${theme.spacing(2, 0, 1)}; `, diff --git a/packages/grafana-ui/src/components/Select/types.ts b/packages/grafana-ui/src/components/Select/types.ts index 3e6d5c4fbfe..311cf4055c4 100644 --- a/packages/grafana-ui/src/components/Select/types.ts +++ b/packages/grafana-ui/src/components/Select/types.ts @@ -119,7 +119,7 @@ export interface VirtualizedSelectAsyncProps export interface MultiSelectCommonProps extends Omit, 'onChange' | 'isMulti' | 'value'> { value?: Array> | T[]; - onChange: (item: Array>) => {} | void; + onChange: (item: Array>, actionMeta: ActionMeta) => {} | void; } // This is the type of *our* SelectBase component, not ReactSelect's prop, although diff --git a/packages/grafana-ui/src/components/VizLegend/VizLegendListItem.tsx b/packages/grafana-ui/src/components/VizLegend/VizLegendListItem.tsx index 4381a3f8f0a..d9ebbaa0b97 100644 --- a/packages/grafana-ui/src/components/VizLegend/VizLegendListItem.tsx +++ b/packages/grafana-ui/src/components/VizLegend/VizLegendListItem.tsx @@ -99,6 +99,7 @@ const getStyles = (theme: GrafanaTheme2) => ({ border: none; font-size: inherit; padding: 0; + user-select: text; `, itemDisabled: css` label: LegendLabelDisabled; diff --git a/packages/grafana-ui/src/components/uPlot/config/UPlotScaleBuilder.ts b/packages/grafana-ui/src/components/uPlot/config/UPlotScaleBuilder.ts index cbdfdbd4539..2011ad0d147 100644 --- a/packages/grafana-ui/src/components/uPlot/config/UPlotScaleBuilder.ts +++ b/packages/grafana-ui/src/components/uPlot/config/UPlotScaleBuilder.ts @@ -116,7 +116,7 @@ export class UPlotScaleBuilder extends PlotConfigBuilder { minMax = uPlot.rangeNum(hardMinOnly ? hardMin : dataMin, hardMaxOnly ? hardMax : dataMax, rangeConfig); } } else if (scale.distr === 3) { - minMax = uPlot.rangeLog(dataMin!, dataMax!, logBase, true); + minMax = uPlot.rangeLog(hardMin ?? dataMin!, hardMax ?? dataMax!, logBase, true); } if (decimals === 0) { @@ -154,13 +154,15 @@ export class UPlotScaleBuilder extends PlotConfigBuilder { } } - // if all we got were hard limits, treat them as static min/max - if (hardMinOnly) { - minMax[0] = hardMin!; - } + if (scale.distr === 1) { + // if all we got were hard limits, treat them as static min/max + if (hardMinOnly) { + minMax[0] = hardMin!; + } - if (hardMaxOnly) { - minMax[1] = hardMax!; + if (hardMaxOnly) { + minMax[1] = hardMax!; + } } // guard against invalid y ranges diff --git a/packages/jaeger-ui-components/package.json b/packages/jaeger-ui-components/package.json deleted file mode 100644 index 8984f23da5d..00000000000 --- a/packages/jaeger-ui-components/package.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "name": "@jaegertracing/jaeger-ui-components", - "version": "9.4.0-pre", - "main": "src/index.ts", - "types": "src/index.ts", - "license": "Apache-2.0", - "private": true, - "scripts": { - "typecheck": "tsc --noEmit" - }, - "devDependencies": { - "@grafana/tsconfig": "^1.2.0-rc1", - "@testing-library/jest-dom": "5.16.5", - "@testing-library/react": "12.1.4", - "@testing-library/user-event": "14.4.3", - "@types/deep-freeze": "^0.1.1", - "@types/hoist-non-react-statics": "^3.3.1", - "@types/jest": "29.2.3", - "@types/lodash": "4.14.187", - "@types/prop-types": "15.7.5", - "@types/react": "17.0.42", - "@types/react-icons": "2.2.7", - "@types/sinon": "^10.0.13", - "@types/slate-react": "0.22.9", - "@types/testing-library__jest-dom": "5.14.5", - "@types/tinycolor2": "1.4.3", - "sinon": "14.0.1", - "typescript": "4.8.4" - }, - "dependencies": { - "@emotion/css": "11.10.5", - "@grafana/data": "9.4.0-pre", - "@grafana/e2e-selectors": "9.4.0-pre", - "@grafana/runtime": "9.4.0-pre", - "@grafana/ui": "9.4.0-pre", - "chance": "^1.0.10", - "classnames": "^2.2.5", - "combokeys": "^3.0.0", - "copy-to-clipboard": "^3.1.0", - "deep-freeze": "^0.0.1", - "fuzzy": "^0.1.3", - "hoist-non-react-statics": "^3.3.2", - "json-markup": "^1.1.0", - "lodash": "4.17.21", - "lru-memoize": "^1.1.0", - "memoize-one": "6.0.0", - "moment": "2.29.4", - "moment-timezone": "0.5.38", - "prop-types": "15.8.1", - "react": "17.0.2", - "react-dom": "17.0.2", - "react-icons": "2.2.7", - "reselect": "4.1.6", - "tinycolor2": "1.4.2", - "tslib": "2.4.1", - "tween-functions": "^1.2.0" - } -} diff --git a/packages/jaeger-ui-components/src/common/NewWindowIcon.test.tsx b/packages/jaeger-ui-components/src/common/NewWindowIcon.test.tsx deleted file mode 100644 index bde47016e78..00000000000 --- a/packages/jaeger-ui-components/src/common/NewWindowIcon.test.tsx +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) 2019 Uber Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { render } from '@testing-library/react'; -import React from 'react'; - -import NewWindowIcon, { getStyles } from './NewWindowIcon'; - -describe('NewWindowIcon', () => { - it('adds is-large className when props.isLarge is true', () => { - const { container } = render(); - const styles = getStyles(); - expect(container.firstChild).toHaveClass(styles.NewWindowIconLarge); - }); -}); diff --git a/packages/jaeger-ui-components/src/constants/default-config.tsx b/packages/jaeger-ui-components/src/constants/default-config.tsx deleted file mode 100644 index 5dfe3420920..00000000000 --- a/packages/jaeger-ui-components/src/constants/default-config.tsx +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright (c) 2017 Uber Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import deepFreeze from 'deep-freeze'; - -import { FALLBACK_DAG_MAX_NUM_SERVICES } from './index'; - -export default deepFreeze( - Object.defineProperty( - { - archiveEnabled: false, - dependencies: { - dagMaxNumServices: FALLBACK_DAG_MAX_NUM_SERVICES, - menuEnabled: true, - }, - linkPatterns: [], - menu: [ - { - label: 'About Jaeger', - items: [ - { - label: 'GitHub', - url: 'https://github.com/uber/jaeger', - }, - { - label: 'Docs', - url: 'http://jaeger.readthedocs.io/en/latest/', - }, - { - label: 'Twitter', - url: 'https://twitter.com/JaegerTracing', - }, - { - label: 'Discussion Group', - url: 'https://groups.google.com/forum/#!forum/jaeger-tracing', - }, - { - label: 'Gitter.im', - url: 'https://gitter.im/jaegertracing/Lobby', - }, - { - label: 'Blog', - url: 'https://medium.com/jaegertracing/', - }, - ], - }, - ], - search: { - maxLookback: { - label: '2 Days', - value: '2d', - }, - maxLimit: 1500, - }, - tracking: { - gaID: null, - trackErrors: true, - }, - }, - // fields that should be individually merged vs wholesale replaced - '__mergeFields', - { value: ['dependencies', 'search', 'tracking'] } - ) -); - -export const deprecations = [ - { - formerKey: 'dependenciesMenuEnabled', - currentKey: 'dependencies.menuEnabled', - }, - { - formerKey: 'gaTrackingID', - currentKey: 'tracking.gaID', - }, -]; diff --git a/packages/jaeger-ui-components/src/types/TDdgState.tsx b/packages/jaeger-ui-components/src/types/TDdgState.tsx deleted file mode 100644 index f1aba58ac7c..00000000000 --- a/packages/jaeger-ui-components/src/types/TDdgState.tsx +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) 2019 Uber Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { fetchedState } from '../constants'; -import { TDdgModel } from '../model/ddg/types'; - -import { ApiError } from './api-error'; - -export type TDdgStateEntry = - | { - state: typeof fetchedState.LOADING; - } - | { - error: ApiError; - state: typeof fetchedState.ERROR; - } - | { - model: TDdgModel; - state: typeof fetchedState.DONE; - viewModifiers: Map; - }; - -type TDdgState = Record; - -// eslint-disable-next-line no-undef -export default TDdgState; diff --git a/packages/jaeger-ui-components/src/types/embedded.tsx b/packages/jaeger-ui-components/src/types/embedded.tsx deleted file mode 100644 index e7f87776df5..00000000000 --- a/packages/jaeger-ui-components/src/types/embedded.tsx +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) 2018 Uber Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -type EmbeddedStateV0 = { - version: 'v0'; - searchHideGraph: boolean; - timeline: { - collapseTitle: boolean; - hideMinimap: boolean; - hideSummary: boolean; - }; -}; - -export type EmbeddedState = EmbeddedStateV0; diff --git a/packages/jaeger-ui-components/src/types/search.tsx b/packages/jaeger-ui-components/src/types/search.tsx deleted file mode 100644 index 380d4316ddc..00000000000 --- a/packages/jaeger-ui-components/src/types/search.tsx +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (c) 2017 Uber Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { TNil } from '.'; - -export type SearchQuery = { - end: number | string; - limit: number | string; - lookback: string; - maxDuration: null | string; - minDuration: null | string; - operation: string | TNil; - service: string; - start: number | string; - tags: string | TNil; -}; - -/** - * Type used to summarize traces for the search page. - */ -export type TraceSummary = { - /** - * Duration of trace in milliseconds. - */ - duration: number; - /** - * Start time of trace in milliseconds. - */ - timestamp: number; - traceName: string; - traceID: string; - numberOfErredSpans: number; - numberOfSpans: number; - services: Array<{ name: string; numberOfSpans: number }>; -}; - -export type TraceSummaries = { - /** - * Duration of longest trace in `traces` in milliseconds. - */ - maxDuration: number; - traces: TraceSummary[]; -}; diff --git a/packages/jaeger-ui-components/tsconfig.json b/packages/jaeger-ui-components/tsconfig.json deleted file mode 100644 index 83f4d88d032..00000000000 --- a/packages/jaeger-ui-components/tsconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "compilerOptions": { - "baseUrl": ".", - "allowJs": true, - "jsx": "react" - }, - "exclude": ["dist", "node_modules"], - "extends": "@grafana/tsconfig", - "include": [ - "src/**/*.ts*", - "typings", - "../../public/app/types/jquery/*.ts", - "../../public/app/types/*.d.ts", - "../grafana-ui/src/types/*.d.ts" - ] -} diff --git a/pkg/api/accesscontrol.go b/pkg/api/accesscontrol.go index 035cf4ac315..55945b28a4d 100644 --- a/pkg/api/accesscontrol.go +++ b/pkg/api/accesscontrol.go @@ -3,9 +3,9 @@ package api import ( "fmt" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" ac "github.com/grafana/grafana/pkg/services/accesscontrol" + 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/org" @@ -435,7 +435,7 @@ func (hs *HTTPServer) declareFixedRoles() error { // Metadata helpers // getAccessControlMetadata returns the accesscontrol metadata associated with a given resource -func (hs *HTTPServer) getAccessControlMetadata(c *models.ReqContext, +func (hs *HTTPServer) getAccessControlMetadata(c *contextmodel.ReqContext, orgID int64, prefix string, resourceID string) ac.Metadata { ids := map[string]bool{resourceID: true} return hs.getMultiAccessControlMetadata(c, orgID, prefix, ids)[resourceID] @@ -443,7 +443,7 @@ func (hs *HTTPServer) getAccessControlMetadata(c *models.ReqContext, // getMultiAccessControlMetadata returns the accesscontrol metadata associated with a given set of resources // Context must contain permissions in the given org (see LoadPermissionsMiddleware or AuthorizeInOrgMiddleware) -func (hs *HTTPServer) getMultiAccessControlMetadata(c *models.ReqContext, +func (hs *HTTPServer) getMultiAccessControlMetadata(c *contextmodel.ReqContext, orgID int64, prefix string, resourceIDs map[string]bool) map[string]ac.Metadata { if hs.AccessControl.IsDisabled() || !c.QueryBool("accesscontrol") { return map[string]ac.Metadata{} diff --git a/pkg/api/admin.go b/pkg/api/admin.go index 347aa45c20d..19ea7269cbf 100644 --- a/pkg/api/admin.go +++ b/pkg/api/admin.go @@ -5,8 +5,8 @@ import ( "net/http" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" ac "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/stats" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" @@ -25,7 +25,7 @@ import ( // 200: adminGetSettingsResponse // 401: unauthorisedError // 403: forbiddenError -func (hs *HTTPServer) AdminGetSettings(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AdminGetSettings(c *contextmodel.ReqContext) response.Response { settings, err := hs.getAuthorizedSettings(c.Req.Context(), c.SignedInUser, hs.SettingsProvider.Current()) if err != nil { return response.Error(http.StatusForbidden, "Failed to authorize settings", err) @@ -45,7 +45,7 @@ func (hs *HTTPServer) AdminGetSettings(c *models.ReqContext) response.Response { // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) AdminGetStats(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AdminGetStats(c *contextmodel.ReqContext) response.Response { statsQuery := stats.GetAdminStatsQuery{} if err := hs.statsService.GetAdminStats(c.Req.Context(), &statsQuery); err != nil { diff --git a/pkg/api/admin_encryption.go b/pkg/api/admin_encryption.go index 9f00aa2efd5..e6d850d292a 100644 --- a/pkg/api/admin_encryption.go +++ b/pkg/api/admin_encryption.go @@ -5,11 +5,11 @@ import ( "net/http" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" skv "github.com/grafana/grafana/pkg/services/secrets/kvstore" ) -func (hs *HTTPServer) AdminRotateDataEncryptionKeys(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AdminRotateDataEncryptionKeys(c *contextmodel.ReqContext) response.Response { if err := hs.SecretsService.RotateDataKeys(c.Req.Context()); err != nil { return response.Error(http.StatusInternalServerError, "Failed to rotate data keys", err) } @@ -17,7 +17,7 @@ func (hs *HTTPServer) AdminRotateDataEncryptionKeys(c *models.ReqContext) respon return response.Respond(http.StatusNoContent, "") } -func (hs *HTTPServer) AdminReEncryptEncryptionKeys(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AdminReEncryptEncryptionKeys(c *contextmodel.ReqContext) response.Response { if err := hs.SecretsService.ReEncryptDataKeys(c.Req.Context()); err != nil { return response.Error(http.StatusInternalServerError, "Failed to re-encrypt data keys", err) } @@ -25,7 +25,7 @@ func (hs *HTTPServer) AdminReEncryptEncryptionKeys(c *models.ReqContext) respons return response.Respond(http.StatusOK, "Data encryption keys re-encrypted successfully") } -func (hs *HTTPServer) AdminReEncryptSecrets(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AdminReEncryptSecrets(c *contextmodel.ReqContext) response.Response { success, err := hs.secretsMigrator.ReEncryptSecrets(c.Req.Context()) if err != nil { return response.Error(http.StatusInternalServerError, "Failed to re-encrypt secrets", err) @@ -38,7 +38,7 @@ func (hs *HTTPServer) AdminReEncryptSecrets(c *models.ReqContext) response.Respo return response.Respond(http.StatusOK, "Secrets re-encrypted successfully") } -func (hs *HTTPServer) AdminRollbackSecrets(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AdminRollbackSecrets(c *contextmodel.ReqContext) response.Response { success, err := hs.secretsMigrator.RollBackSecrets(c.Req.Context()) if err != nil { return response.Error(http.StatusInternalServerError, "Failed to rollback secrets", err) @@ -53,7 +53,7 @@ func (hs *HTTPServer) AdminRollbackSecrets(c *models.ReqContext) response.Respon // To migrate to the plugin, it must be installed and configured // so as not to lose access to migrated secrets -func (hs *HTTPServer) AdminMigrateSecretsToPlugin(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AdminMigrateSecretsToPlugin(c *contextmodel.ReqContext) response.Response { if skv.EvaluateRemoteSecretsPlugin(c.Req.Context(), hs.secretsPluginManager, hs.Cfg) != nil { hs.log.Warn("Received secrets plugin migration request while plugin is not available") return response.Respond(http.StatusBadRequest, "Secrets plugin is not available") @@ -68,7 +68,7 @@ func (hs *HTTPServer) AdminMigrateSecretsToPlugin(c *models.ReqContext) response // To migrate from the plugin, it must be installed only // as it is possible the user disabled it and then wants to migrate -func (hs *HTTPServer) AdminMigrateSecretsFromPlugin(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AdminMigrateSecretsFromPlugin(c *contextmodel.ReqContext) response.Response { if hs.secretsPluginManager.SecretsManager(c.Req.Context()) == nil { hs.log.Warn("Received secrets plugin migration request while plugin is not installed") return response.Respond(http.StatusBadRequest, "Secrets plugin is not installed") @@ -81,7 +81,7 @@ func (hs *HTTPServer) AdminMigrateSecretsFromPlugin(c *models.ReqContext) respon return response.Respond(http.StatusOK, "Secret migration from plugin triggered successfully") } -func (hs *HTTPServer) AdminDeleteAllSecretsManagerPluginSecrets(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AdminDeleteAllSecretsManagerPluginSecrets(c *contextmodel.ReqContext) response.Response { if hs.secretsPluginManager.SecretsManager(c.Req.Context()) == nil { hs.log.Warn("Received secrets plugin deletion request while plugin is not installed") return response.Respond(http.StatusBadRequest, "Secrets plugin is not installed") diff --git a/pkg/api/admin_provisioning.go b/pkg/api/admin_provisioning.go index d770c0fba02..704a2e51b99 100644 --- a/pkg/api/admin_provisioning.go +++ b/pkg/api/admin_provisioning.go @@ -5,7 +5,7 @@ import ( "errors" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" ) // swagger:route POST /admin/provisioning/dashboards/reload admin_provisioning adminProvisioningReloadDashboards @@ -23,7 +23,7 @@ import ( // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) AdminProvisioningReloadDashboards(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AdminProvisioningReloadDashboards(c *contextmodel.ReqContext) response.Response { err := hs.ProvisioningService.ProvisionDashboards(c.Req.Context()) if err != nil && !errors.Is(err, context.Canceled) { return response.Error(500, "", err) @@ -46,7 +46,7 @@ func (hs *HTTPServer) AdminProvisioningReloadDashboards(c *models.ReqContext) re // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) AdminProvisioningReloadDatasources(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AdminProvisioningReloadDatasources(c *contextmodel.ReqContext) response.Response { err := hs.ProvisioningService.ProvisionDatasources(c.Req.Context()) if err != nil { return response.Error(500, "", err) @@ -69,7 +69,7 @@ func (hs *HTTPServer) AdminProvisioningReloadDatasources(c *models.ReqContext) r // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) AdminProvisioningReloadPlugins(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AdminProvisioningReloadPlugins(c *contextmodel.ReqContext) response.Response { err := hs.ProvisioningService.ProvisionPlugins(c.Req.Context()) if err != nil { return response.Error(500, "Failed to reload plugins config", err) @@ -92,7 +92,7 @@ func (hs *HTTPServer) AdminProvisioningReloadPlugins(c *models.ReqContext) respo // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) AdminProvisioningReloadNotifications(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AdminProvisioningReloadNotifications(c *contextmodel.ReqContext) response.Response { err := hs.ProvisioningService.ProvisionNotifications(c.Req.Context()) if err != nil { return response.Error(500, "", err) @@ -100,7 +100,7 @@ func (hs *HTTPServer) AdminProvisioningReloadNotifications(c *models.ReqContext) return response.Success("Notifications config reloaded") } -func (hs *HTTPServer) AdminProvisioningReloadAlerting(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AdminProvisioningReloadAlerting(c *contextmodel.ReqContext) response.Response { err := hs.ProvisioningService.ProvisionAlerting(c.Req.Context()) if err != nil { return response.Error(500, "", err) diff --git a/pkg/api/admin_test.go b/pkg/api/admin_test.go index 8c63fbed185..04c69f4755b 100644 --- a/pkg/api/admin_test.go +++ b/pkg/api/admin_test.go @@ -5,10 +5,10 @@ import ( "net/http" "testing" - "github.com/grafana/grafana/pkg/infra/db/dbtest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/db/dbtest" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/stats/statstest" "github.com/grafana/grafana/pkg/setting" diff --git a/pkg/api/admin_users.go b/pkg/api/admin_users.go index 18628f1114b..0a437904f9c 100644 --- a/pkg/api/admin_users.go +++ b/pkg/api/admin_users.go @@ -15,6 +15,8 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/auth" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" + "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/util" @@ -38,7 +40,7 @@ import ( // 403: forbiddenError // 412: preconditionFailedError // 500: internalServerError -func (hs *HTTPServer) AdminCreateUser(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AdminCreateUser(c *contextmodel.ReqContext) response.Response { form := dtos.AdminCreateUserForm{} if err := web.Bind(c.Req, &form); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -104,7 +106,7 @@ func (hs *HTTPServer) AdminCreateUser(c *models.ReqContext) response.Response { // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) AdminUpdateUserPassword(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AdminUpdateUserPassword(c *contextmodel.ReqContext) response.Response { form := dtos.AdminUpdateUserPasswordForm{} if err := web.Bind(c.Req, &form); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -156,7 +158,7 @@ func (hs *HTTPServer) AdminUpdateUserPassword(c *models.ReqContext) response.Res // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) AdminUpdateUserPermissions(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AdminUpdateUserPermissions(c *contextmodel.ReqContext) response.Response { form := dtos.AdminUpdateUserPermissionsForm{} if err := web.Bind(c.Req, &form); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -193,7 +195,7 @@ func (hs *HTTPServer) AdminUpdateUserPermissions(c *models.ReqContext) response. // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) AdminDeleteUser(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AdminDeleteUser(c *contextmodel.ReqContext) response.Response { userID, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "id is invalid", err) @@ -285,14 +287,14 @@ func (hs *HTTPServer) AdminDeleteUser(c *models.ReqContext) response.Response { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) AdminDisableUser(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AdminDisableUser(c *contextmodel.ReqContext) response.Response { userID, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "id is invalid", err) } // External users shouldn't be disabled from API - authInfoQuery := &models.GetAuthInfoQuery{UserId: userID} + authInfoQuery := &login.GetAuthInfoQuery{UserId: userID} if err := hs.authInfoService.GetAuthInfo(c.Req.Context(), authInfoQuery); !errors.Is(err, user.ErrUserNotFound) { return response.Error(500, "Could not disable external user", nil) } @@ -328,14 +330,14 @@ func (hs *HTTPServer) AdminDisableUser(c *models.ReqContext) response.Response { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) AdminEnableUser(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AdminEnableUser(c *contextmodel.ReqContext) response.Response { userID, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "id is invalid", err) } // External users shouldn't be disabled from API - authInfoQuery := &models.GetAuthInfoQuery{UserId: userID} + authInfoQuery := &login.GetAuthInfoQuery{UserId: userID} if err := hs.authInfoService.GetAuthInfo(c.Req.Context(), authInfoQuery); !errors.Is(err, user.ErrUserNotFound) { return response.Error(500, "Could not enable external user", nil) } @@ -366,7 +368,7 @@ func (hs *HTTPServer) AdminEnableUser(c *models.ReqContext) response.Response { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) AdminLogoutUser(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AdminLogoutUser(c *contextmodel.ReqContext) response.Response { userID, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "id is invalid", err) @@ -392,7 +394,7 @@ func (hs *HTTPServer) AdminLogoutUser(c *models.ReqContext) response.Response { // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) AdminGetUserAuthTokens(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AdminGetUserAuthTokens(c *contextmodel.ReqContext) response.Response { userID, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "id is invalid", err) @@ -417,7 +419,7 @@ func (hs *HTTPServer) AdminGetUserAuthTokens(c *models.ReqContext) response.Resp // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) AdminRevokeUserAuthToken(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AdminRevokeUserAuthToken(c *contextmodel.ReqContext) response.Response { cmd := auth.RevokeAuthTokenCmd{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) diff --git a/pkg/api/admin_users_test.go b/pkg/api/admin_users_test.go index b6e72dc0e59..f6d2fb0aa35 100644 --- a/pkg/api/admin_users_test.go +++ b/pkg/api/admin_users_test.go @@ -13,9 +13,9 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/db/dbtest" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/auth/authtest" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/login/logintest" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" @@ -242,7 +242,7 @@ func putAdminScenario(t *testing.T, desc string, url string, routePattern string } sc := setupScenarioContext(t, url) - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(cmd) c.Req.Header.Add("Content-Type", "application/json") sc.context = c @@ -267,7 +267,7 @@ func adminLogoutUserScenario(t *testing.T, desc string, url string, routePattern } sc := setupScenarioContext(t, url) - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { t.Log("Route handler invoked", "url", c.Req.URL) sc.context = c @@ -295,7 +295,7 @@ func adminRevokeUserAuthTokenScenario(t *testing.T, desc string, url string, rou sc := setupScenarioContext(t, url) sc.userAuthTokenService = fakeAuthTokenService - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(cmd) c.Req.Header.Add("Content-Type", "application/json") sc.context = c @@ -323,7 +323,7 @@ func adminGetUserAuthTokensScenario(t *testing.T, desc string, url string, route sc := setupScenarioContext(t, url) sc.userAuthTokenService = fakeAuthTokenService - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { sc.context = c sc.context.UserID = testUserID sc.context.OrgID = testOrgID @@ -355,7 +355,7 @@ func adminDisableUserScenario(t *testing.T, desc string, action string, url stri sc.sqlStore = hs.SQLStore sc.authInfoService = authInfoService sc.userService = hs.userService - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { sc.context = c sc.context.UserID = testUserID @@ -381,7 +381,7 @@ func adminDeleteUserScenario(t *testing.T, desc string, url string, routePattern sc := setupScenarioContext(t, url) sc.sqlStore = hs.SQLStore sc.authInfoService = &logintest.AuthInfoServiceFake{} - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { sc.context = c sc.context.UserID = testUserID @@ -402,7 +402,7 @@ func adminCreateUserScenario(t *testing.T, desc string, url string, routePattern } sc := setupScenarioContext(t, url) - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(cmd) c.Req.Header.Add("Content-Type", "application/json") sc.context = c diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go index 86fa1884d80..bda411036c0 100644 --- a/pkg/api/alerting.go +++ b/pkg/api/alerting.go @@ -9,21 +9,22 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" alertmodels "github.com/grafana/grafana/pkg/services/alerting/models" + 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/guardian" "github.com/grafana/grafana/pkg/services/ngalert/notifier/channels_config" "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/services/search" + "github.com/grafana/grafana/pkg/services/search/model" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/web" ) -func (hs *HTTPServer) ValidateOrgAlert(c *models.ReqContext) { +func (hs *HTTPServer) ValidateOrgAlert(c *contextmodel.ReqContext) { id, err := strconv.ParseInt(web.Params(c.Req)[":alertId"], 10, 64) if err != nil { c.JsonApiErr(http.StatusBadRequest, "alertId is invalid", nil) @@ -51,7 +52,7 @@ func (hs *HTTPServer) ValidateOrgAlert(c *models.ReqContext) { // 200: getDashboardStatesResponse // 400: badRequestError // 500: internalServerError -func (hs *HTTPServer) GetAlertStatesForDashboard(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetAlertStatesForDashboard(c *contextmodel.ReqContext) response.Response { dashboardID := c.QueryInt64("dashboardId") if dashboardID == 0 { @@ -78,7 +79,7 @@ func (hs *HTTPServer) GetAlertStatesForDashboard(c *models.ReqContext) response. // 200: getAlertsResponse // 401: unauthorisedError // 500: internalServerError -func (hs *HTTPServer) GetAlerts(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetAlerts(c *contextmodel.ReqContext) response.Response { dashboardQuery := c.Query("dashboardQuery") dashboardTags := c.QueryStrings("dashboardTag") stringDashboardIDs := c.QueryStrings("dashboardId") @@ -108,7 +109,7 @@ func (hs *HTTPServer) GetAlerts(c *models.ReqContext) response.Response { Limit: 1000, OrgId: c.OrgID, DashboardIds: dashboardIDs, - Type: string(models.DashHitDB), + Type: string(model.DashHitDB), FolderIds: folderIDs, Permission: dashboards.PERMISSION_VIEW, } @@ -119,7 +120,7 @@ func (hs *HTTPServer) GetAlerts(c *models.ReqContext) response.Response { } for _, d := range searchQuery.Result { - if d.Type == models.DashHitDB && d.ID > 0 { + if d.Type == model.DashHitDB && d.ID > 0 { dashboardIDs = append(dashboardIDs, d.ID) } } @@ -165,7 +166,7 @@ func (hs *HTTPServer) GetAlerts(c *models.ReqContext) response.Response { // 422: unprocessableEntityError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) AlertTest(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AlertTest(c *contextmodel.ReqContext) response.Response { dto := dtos.AlertTestCommand{} if err := web.Bind(c.Req, &dto); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -219,7 +220,7 @@ func (hs *HTTPServer) AlertTest(c *models.ReqContext) response.Response { // 200: getAlertResponse // 401: unauthorisedError // 500: internalServerError -func (hs *HTTPServer) GetAlert(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetAlert(c *contextmodel.ReqContext) response.Response { id, err := strconv.ParseInt(web.Params(c.Req)[":alertId"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "alertId is invalid", err) @@ -233,8 +234,8 @@ func (hs *HTTPServer) GetAlert(c *models.ReqContext) response.Response { return response.JSON(http.StatusOK, &query.Result) } -func (hs *HTTPServer) GetAlertNotifiers(ngalertEnabled bool) func(*models.ReqContext) response.Response { - return func(_ *models.ReqContext) response.Response { +func (hs *HTTPServer) GetAlertNotifiers(ngalertEnabled bool) func(*contextmodel.ReqContext) response.Response { + return func(_ *contextmodel.ReqContext) response.Response { if ngalertEnabled { return response.JSON(http.StatusOK, channels_config.GetAvailableNotifiers()) } @@ -255,7 +256,7 @@ func (hs *HTTPServer) GetAlertNotifiers(ngalertEnabled bool) func(*models.ReqCon // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) GetAlertNotificationLookup(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetAlertNotificationLookup(c *contextmodel.ReqContext) response.Response { alertNotifications, err := hs.getAlertNotificationsInternal(c) if err != nil { return response.Error(500, "Failed to get alert notifications", err) @@ -281,7 +282,7 @@ func (hs *HTTPServer) GetAlertNotificationLookup(c *models.ReqContext) response. // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) GetAlertNotifications(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetAlertNotifications(c *contextmodel.ReqContext) response.Response { alertNotifications, err := hs.getAlertNotificationsInternal(c) if err != nil { return response.Error(500, "Failed to get alert notifications", err) @@ -296,7 +297,7 @@ func (hs *HTTPServer) GetAlertNotifications(c *models.ReqContext) response.Respo return response.JSON(http.StatusOK, result) } -func (hs *HTTPServer) getAlertNotificationsInternal(c *models.ReqContext) ([]*alertmodels.AlertNotification, error) { +func (hs *HTTPServer) getAlertNotificationsInternal(c *contextmodel.ReqContext) ([]*alertmodels.AlertNotification, error) { query := &alertmodels.GetAllAlertNotificationsQuery{OrgId: c.OrgID} if err := hs.AlertNotificationService.GetAllAlertNotifications(c.Req.Context(), query); err != nil { @@ -318,7 +319,7 @@ func (hs *HTTPServer) getAlertNotificationsInternal(c *models.ReqContext) ([]*al // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetAlertNotificationByID(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetAlertNotificationByID(c *contextmodel.ReqContext) response.Response { notificationId, err := strconv.ParseInt(web.Params(c.Req)[":notificationId"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "notificationId is invalid", err) @@ -355,7 +356,7 @@ func (hs *HTTPServer) GetAlertNotificationByID(c *models.ReqContext) response.Re // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetAlertNotificationByUID(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetAlertNotificationByUID(c *contextmodel.ReqContext) response.Response { query := &alertmodels.GetAlertNotificationsWithUidQuery{ OrgId: c.OrgID, Uid: web.Params(c.Req)[":uid"], @@ -388,7 +389,7 @@ func (hs *HTTPServer) GetAlertNotificationByUID(c *models.ReqContext) response.R // 403: forbiddenError // 409: conflictError // 500: internalServerError -func (hs *HTTPServer) CreateAlertNotification(c *models.ReqContext) response.Response { +func (hs *HTTPServer) CreateAlertNotification(c *contextmodel.ReqContext) response.Response { cmd := alertmodels.CreateAlertNotificationCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -421,7 +422,7 @@ func (hs *HTTPServer) CreateAlertNotification(c *models.ReqContext) response.Res // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) UpdateAlertNotification(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UpdateAlertNotification(c *contextmodel.ReqContext) response.Response { cmd := alertmodels.UpdateAlertNotificationCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -468,7 +469,7 @@ func (hs *HTTPServer) UpdateAlertNotification(c *models.ReqContext) response.Res // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) UpdateAlertNotificationByUID(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UpdateAlertNotificationByUID(c *contextmodel.ReqContext) response.Response { cmd := alertmodels.UpdateAlertNotificationWithUidCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -568,7 +569,7 @@ func (hs *HTTPServer) fillWithSecureSettingsDataByUID(ctx context.Context, cmd * // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) DeleteAlertNotification(c *models.ReqContext) response.Response { +func (hs *HTTPServer) DeleteAlertNotification(c *contextmodel.ReqContext) response.Response { notificationId, err := strconv.ParseInt(web.Params(c.Req)[":notificationId"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "notificationId is invalid", err) @@ -601,7 +602,7 @@ func (hs *HTTPServer) DeleteAlertNotification(c *models.ReqContext) response.Res // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) DeleteAlertNotificationByUID(c *models.ReqContext) response.Response { +func (hs *HTTPServer) DeleteAlertNotificationByUID(c *contextmodel.ReqContext) response.Response { cmd := alertmodels.DeleteAlertNotificationWithUidCommand{ OrgId: c.OrgID, Uid: web.Params(c.Req)[":uid"], @@ -633,7 +634,7 @@ func (hs *HTTPServer) DeleteAlertNotificationByUID(c *models.ReqContext) respons // 403: forbiddenError // 412: SMTPNotEnabledError // 500: internalServerError -func (hs *HTTPServer) NotificationTest(c *models.ReqContext) response.Response { +func (hs *HTTPServer) NotificationTest(c *contextmodel.ReqContext) response.Response { dto := dtos.NotificationTestCommand{} if err := web.Bind(c.Req, &dto); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -672,14 +673,14 @@ func (hs *HTTPServer) NotificationTest(c *models.ReqContext) response.Response { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) PauseAlert(legacyAlertingEnabled *bool) func(c *models.ReqContext) response.Response { +func (hs *HTTPServer) PauseAlert(legacyAlertingEnabled *bool) func(c *contextmodel.ReqContext) response.Response { if legacyAlertingEnabled == nil || !*legacyAlertingEnabled { - return func(_ *models.ReqContext) response.Response { + return func(_ *contextmodel.ReqContext) response.Response { return response.Error(http.StatusBadRequest, "legacy alerting is disabled, so this call has no effect.", nil) } } - return func(c *models.ReqContext) response.Response { + return func(c *contextmodel.ReqContext) response.Response { dto := dtos.PauseAlertCommand{} if err := web.Bind(c.Req, &dto); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -754,14 +755,14 @@ func (hs *HTTPServer) PauseAlert(legacyAlertingEnabled *bool) func(c *models.Req // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) PauseAllAlerts(legacyAlertingEnabled *bool) func(c *models.ReqContext) response.Response { +func (hs *HTTPServer) PauseAllAlerts(legacyAlertingEnabled *bool) func(c *contextmodel.ReqContext) response.Response { if legacyAlertingEnabled == nil || !*legacyAlertingEnabled { - return func(_ *models.ReqContext) response.Response { + return func(_ *contextmodel.ReqContext) response.Response { return response.Error(http.StatusBadRequest, "legacy alerting is disabled, so this call has no effect.", nil) } } - return func(c *models.ReqContext) response.Response { + return func(c *contextmodel.ReqContext) response.Response { dto := dtos.PauseAllAlertsCommand{} if err := web.Bind(c.Req, &dto); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) diff --git a/pkg/api/annotations.go b/pkg/api/annotations.go index cddf8d1529e..4937349c09e 100644 --- a/pkg/api/annotations.go +++ b/pkg/api/annotations.go @@ -9,9 +9,9 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/annotations" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/guardian" @@ -31,7 +31,7 @@ import ( // 200: getAnnotationsResponse // 401: unauthorisedError // 500: internalServerError -func (hs *HTTPServer) GetAnnotations(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetAnnotations(c *contextmodel.ReqContext) response.Response { query := &annotations.ItemQuery{ From: c.QueryInt64("from"), To: c.QueryInt64("to"), @@ -114,7 +114,7 @@ func (e *AnnotationError) Error() string { // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) PostAnnotation(c *models.ReqContext) response.Response { +func (hs *HTTPServer) PostAnnotation(c *contextmodel.ReqContext) response.Response { cmd := dtos.PostAnnotationsCmd{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -185,7 +185,7 @@ func formatGraphiteAnnotation(what string, data string) string { // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) PostGraphiteAnnotation(c *models.ReqContext) response.Response { +func (hs *HTTPServer) PostGraphiteAnnotation(c *contextmodel.ReqContext) response.Response { cmd := dtos.PostGraphiteAnnotationsCmd{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -250,7 +250,7 @@ func (hs *HTTPServer) PostGraphiteAnnotation(c *models.ReqContext) response.Resp // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) UpdateAnnotation(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UpdateAnnotation(c *contextmodel.ReqContext) response.Response { cmd := dtos.UpdateAnnotationsCmd{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -306,7 +306,7 @@ func (hs *HTTPServer) UpdateAnnotation(c *models.ReqContext) response.Response { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) PatchAnnotation(c *models.ReqContext) response.Response { +func (hs *HTTPServer) PatchAnnotation(c *contextmodel.ReqContext) response.Response { cmd := dtos.PatchAnnotationsCmd{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -371,7 +371,7 @@ func (hs *HTTPServer) PatchAnnotation(c *models.ReqContext) response.Response { // 200: okResponse // 401: unauthorisedError // 500: internalServerError -func (hs *HTTPServer) MassDeleteAnnotations(c *models.ReqContext) response.Response { +func (hs *HTTPServer) MassDeleteAnnotations(c *contextmodel.ReqContext) response.Response { cmd := dtos.MassDeleteAnnotationsCmd{} err := web.Bind(c.Req, &cmd) if err != nil { @@ -447,7 +447,7 @@ func (hs *HTTPServer) MassDeleteAnnotations(c *models.ReqContext) response.Respo // 200: getAnnotationByIDResponse // 401: unauthorisedError // 500: internalServerError -func (hs *HTTPServer) GetAnnotationByID(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetAnnotationByID(c *contextmodel.ReqContext) response.Response { annotationID, err := strconv.ParseInt(web.Params(c.Req)[":annotationId"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "annotationId is invalid", err) @@ -476,7 +476,7 @@ func (hs *HTTPServer) GetAnnotationByID(c *models.ReqContext) response.Response // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) DeleteAnnotationByID(c *models.ReqContext) response.Response { +func (hs *HTTPServer) DeleteAnnotationByID(c *contextmodel.ReqContext) response.Response { annotationID, err := strconv.ParseInt(web.Params(c.Req)[":annotationId"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "annotationId is invalid", err) @@ -502,7 +502,7 @@ func (hs *HTTPServer) DeleteAnnotationByID(c *models.ReqContext) response.Respon return response.Success("Annotation deleted") } -func (hs *HTTPServer) canSaveAnnotation(c *models.ReqContext, annotation *annotations.ItemDTO) (bool, error) { +func (hs *HTTPServer) canSaveAnnotation(c *contextmodel.ReqContext, annotation *annotations.ItemDTO) (bool, error) { if annotation.GetType() == annotations.Dashboard { return canEditDashboard(c, annotation.DashboardId) } else { @@ -513,7 +513,7 @@ func (hs *HTTPServer) canSaveAnnotation(c *models.ReqContext, annotation *annota } } -func canEditDashboard(c *models.ReqContext, dashboardID int64) (bool, error) { +func canEditDashboard(c *contextmodel.ReqContext, dashboardID int64) (bool, error) { guard, err := guardian.New(c.Req.Context(), dashboardID, c.OrgID, c.SignedInUser) if err != nil { return false, err @@ -555,7 +555,7 @@ func findAnnotationByID(ctx context.Context, repo annotations.Repository, annota // 200: getAnnotationTagsResponse // 401: unauthorisedError // 500: internalServerError -func (hs *HTTPServer) GetAnnotationTags(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetAnnotationTags(c *contextmodel.ReqContext) response.Response { query := &annotations.TagsQuery{ OrgID: c.OrgID, Tag: c.Query("tag"), @@ -612,7 +612,7 @@ func AnnotationTypeScopeResolver(annotationsRepo annotations.Repository) (string }) } -func (hs *HTTPServer) canCreateAnnotation(c *models.ReqContext, dashboardId int64) (bool, error) { +func (hs *HTTPServer) canCreateAnnotation(c *contextmodel.ReqContext, dashboardId int64) (bool, error) { if dashboardId != 0 { if !hs.AccessControl.IsDisabled() { evaluator := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsCreate, accesscontrol.ScopeAnnotationsTypeDashboard) @@ -632,7 +632,7 @@ func (hs *HTTPServer) canCreateAnnotation(c *models.ReqContext, dashboardId int6 } } -func (hs *HTTPServer) canMassDeleteAnnotations(c *models.ReqContext, dashboardID int64) (bool, error) { +func (hs *HTTPServer) canMassDeleteAnnotations(c *contextmodel.ReqContext, dashboardID int64) (bool, error) { if dashboardID == 0 { evaluator := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsDelete, accesscontrol.ScopeAnnotationsTypeOrganization) return hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluator) diff --git a/pkg/api/annotations_test.go b/pkg/api/annotations_test.go index f5c015f03ff..0ec81d2c44a 100644 --- a/pkg/api/annotations_test.go +++ b/pkg/api/annotations_test.go @@ -17,11 +17,11 @@ import ( "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/db/dbtest" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/annotations/annotationstest" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/org" @@ -296,7 +296,7 @@ func postAnnotationScenario(t *testing.T, desc string, url string, routePattern sc := setupScenarioContext(t, url) sc.dashboardService = dashSvc - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(cmd) c.Req.Header.Add("Content-Type", "application/json") sc.context = c @@ -320,7 +320,7 @@ func putAnnotationScenario(t *testing.T, desc string, url string, routePattern s hs.SQLStore = store sc := setupScenarioContext(t, url) - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(cmd) c.Req.Header.Add("Content-Type", "application/json") sc.context = c @@ -345,7 +345,7 @@ func patchAnnotationScenario(t *testing.T, desc string, url string, routePattern hs.SQLStore = store sc := setupScenarioContext(t, url) - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(cmd) c.Req.Header.Add("Content-Type", "application/json") sc.context = c @@ -370,7 +370,7 @@ func deleteAnnotationsScenario(t *testing.T, desc string, url string, routePatte hs.DashboardService = dashSvc sc := setupScenarioContext(t, url) - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(cmd) c.Req.Header.Add("Content-Type", "application/json") sc.context = c diff --git a/pkg/api/api.go b/pkg/api/api.go index 807c33fe786..154c4156930 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -33,11 +33,11 @@ import ( "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/apikey" "github.com/grafana/grafana/pkg/services/auth" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/correlations" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/datasources" @@ -173,8 +173,8 @@ func (hs *HTTPServer) registerRoutes() { ) } - r.Get("/explore", authorize(func(c *models.ReqContext) { - if f, ok := reqSignedIn.(func(c *models.ReqContext)); ok { + r.Get("/explore", authorize(func(c *contextmodel.ReqContext) { + if f, ok := reqSignedIn.(func(c *contextmodel.ReqContext)); ok { f(c) } middleware.EnsureEditorOrViewerCanEdit(c) diff --git a/pkg/api/apikey.go b/pkg/api/apikey.go index 79a89680f8a..02ba9b6cfce 100644 --- a/pkg/api/apikey.go +++ b/pkg/api/apikey.go @@ -9,8 +9,8 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/components/apikeygen" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/apikey" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/web" ) @@ -26,7 +26,7 @@ import ( // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetAPIKeys(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetAPIKeys(c *contextmodel.ReqContext) response.Response { query := apikey.GetApiKeysQuery{OrgId: c.OrgID, User: c.SignedInUser, IncludeExpired: c.QueryBool("includeExpired")} if err := hs.apiKeyService.GetAPIKeys(c.Req.Context(), &query); err != nil { @@ -70,7 +70,7 @@ func (hs *HTTPServer) GetAPIKeys(c *models.ReqContext) response.Response { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) DeleteAPIKey(c *models.ReqContext) response.Response { +func (hs *HTTPServer) DeleteAPIKey(c *contextmodel.ReqContext) response.Response { id, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "id is invalid", err) @@ -104,7 +104,7 @@ func (hs *HTTPServer) DeleteAPIKey(c *models.ReqContext) response.Response { // 403: forbiddenError // 409: conflictError // 500: internalServerError -func (hs *HTTPServer) AddAPIKey(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AddAPIKey(c *contextmodel.ReqContext) response.Response { cmd := apikey.AddCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) diff --git a/pkg/api/avatar/avatar.go b/pkg/api/avatar/avatar.go index 6ba3e485c88..d331cee8664 100644 --- a/pkg/api/avatar/avatar.go +++ b/pkg/api/avatar/avatar.go @@ -20,11 +20,12 @@ import ( "sync" "time" + gocache "github.com/patrickmn/go-cache" + "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web" - gocache "github.com/patrickmn/go-cache" ) const ( @@ -98,7 +99,7 @@ type AvatarCacheServer struct { var validMD5 = regexp.MustCompile("^[a-fA-F0-9]{32}$") -func (a *AvatarCacheServer) Handler(ctx *models.ReqContext) { +func (a *AvatarCacheServer) Handler(ctx *contextmodel.ReqContext) { hash := web.Params(ctx.Req)[":hash"] if len(hash) != 32 || !validMD5.MatchString(hash) { diff --git a/pkg/api/avatar/avatar_test.go b/pkg/api/avatar/avatar_test.go index abc98a59b77..916ba082e22 100644 --- a/pkg/api/avatar/avatar_test.go +++ b/pkg/api/avatar/avatar_test.go @@ -7,8 +7,9 @@ import ( "testing" "time" - "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/setting" ) const DEFAULT_NONSENSE_HASH string = "9e107d9d372bb6826bd81d3542a419d6" diff --git a/pkg/api/comments.go b/pkg/api/comments.go index b1f7a88f8f6..3a3d3c52b42 100644 --- a/pkg/api/comments.go +++ b/pkg/api/comments.go @@ -5,14 +5,14 @@ import ( "net/http" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/comments" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/web" ) -func (hs *HTTPServer) commentsGet(c *models.ReqContext) response.Response { +func (hs *HTTPServer) commentsGet(c *contextmodel.ReqContext) response.Response { cmd := comments.GetCmd{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -29,7 +29,7 @@ func (hs *HTTPServer) commentsGet(c *models.ReqContext) response.Response { }) } -func (hs *HTTPServer) commentsCreate(c *models.ReqContext) response.Response { +func (hs *HTTPServer) commentsCreate(c *contextmodel.ReqContext) response.Response { cmd := comments.CreateCmd{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index 5c32a3f6f89..9eaa2cefceb 100644 --- a/pkg/api/common_test.go +++ b/pkg/api/common_test.go @@ -23,7 +23,6 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/remotecache" "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" @@ -35,6 +34,7 @@ import ( "github.com/grafana/grafana/pkg/services/contexthandler" "github.com/grafana/grafana/pkg/services/contexthandler/authproxy" "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" dashboardsstore "github.com/grafana/grafana/pkg/services/dashboards/database" dashboardservice "github.com/grafana/grafana/pkg/services/dashboards/service" @@ -52,6 +52,7 @@ import ( "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/services/search" + "github.com/grafana/grafana/pkg/services/search/model" "github.com/grafana/grafana/pkg/services/searchusers" "github.com/grafana/grafana/pkg/services/searchusers/filters" "github.com/grafana/grafana/pkg/services/sqlstore" @@ -76,7 +77,7 @@ func loggedInUserScenarioWithRole(t *testing.T, desc string, method string, url sc := setupScenarioContext(t, url) sc.sqlStore = sqlStore sc.userService = usertest.NewUserServiceFake() - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { sc.context = c sc.context.UserID = testUserID sc.context.OrgID = testOrgID @@ -102,7 +103,7 @@ func loggedInUserScenarioWithRole(t *testing.T, desc string, method string, url func anonymousUserScenario(t *testing.T, desc string, method string, url string, routePattern string, fn scenarioFunc) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { sc := setupScenarioContext(t, url) - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { sc.context = c if sc.handlerFunc != nil { return sc.handlerFunc(sc.context) @@ -178,7 +179,7 @@ type scenarioContext struct { t *testing.T cfg *setting.Cfg m *web.Mux - context *models.ReqContext + context *contextmodel.ReqContext resp *httptest.ResponseRecorder handlerFunc handlerFunc defaultHandler web.Handler @@ -197,7 +198,7 @@ func (sc *scenarioContext) exec() { } type scenarioFunc func(c *scenarioContext) -type handlerFunc func(c *models.ReqContext) response.Response +type handlerFunc func(c *contextmodel.ReqContext) response.Response func getContextHandler(t *testing.T, cfg *setting.Cfg) *contexthandler.ContextHandler { t.Helper() @@ -266,7 +267,7 @@ type accessControlScenarioContext struct { // initCtx is used in a middleware to set the initial context // of the request server side. Can be used to pretend sign in. - initCtx *models.ReqContext + initCtx *contextmodel.ReqContext // hs is a minimal HTTPServer for the accesscontrol tests to pass. hs *HTTPServer @@ -302,17 +303,17 @@ func userWithPermissions(orgID int64, permissions []accesscontrol.Permission) *u } // setInitCtxSignedInUser sets a copy of the user in initCtx -func setInitCtxSignedInUser(initCtx *models.ReqContext, user user.SignedInUser) { +func setInitCtxSignedInUser(initCtx *contextmodel.ReqContext, user user.SignedInUser) { initCtx.IsSignedIn = true initCtx.SignedInUser = &user } -func setInitCtxSignedInViewer(initCtx *models.ReqContext) { +func setInitCtxSignedInViewer(initCtx *contextmodel.ReqContext) { initCtx.IsSignedIn = true initCtx.SignedInUser = &user.SignedInUser{UserID: testUserID, OrgID: 1, OrgRole: org.RoleViewer, Login: testUserLogin} } -func setInitCtxSignedInOrgAdmin(initCtx *models.ReqContext) { +func setInitCtxSignedInOrgAdmin(initCtx *contextmodel.ReqContext) { initCtx.IsSignedIn = true initCtx.SignedInUser = &user.SignedInUser{UserID: testUserID, OrgID: 1, OrgRole: org.RoleAdmin, Login: testUserLogin} } @@ -434,7 +435,7 @@ func setupHTTPServerWithCfgDb( m := web.New() // middleware to set the test initial context - initCtx := &models.ReqContext{} + initCtx := &contextmodel.ReqContext{} m.Use(func(c *web.Context) { initCtx.Context = c initCtx.Logger = log.New("api-test") @@ -523,13 +524,13 @@ type setUpConf struct { aclMockResp []*dashboards.DashboardACLInfoDTO } -type mockSearchService struct{ ExpectedResult models.HitList } +type mockSearchService struct{ ExpectedResult model.HitList } func (mss *mockSearchService) SearchHandler(_ context.Context, q *search.Query) error { q.Result = mss.ExpectedResult return nil } -func (mss *mockSearchService) SortOptions() []models.SortOption { return nil } +func (mss *mockSearchService) SortOptions() []model.SortOption { return nil } func setUp(confs ...setUpConf) *HTTPServer { store := dbtest.NewFakeDB() diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 95b1e98960c..ce2ca9d31aa 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -18,9 +18,9 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/kinds/dashboard" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/alerting" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" dashver "github.com/grafana/grafana/pkg/services/dashboardversion" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -39,7 +39,7 @@ const ( anonString = "Anonymous" ) -func (hs *HTTPServer) isDashboardStarredByUser(c *models.ReqContext, dashID int64) (bool, error) { +func (hs *HTTPServer) isDashboardStarredByUser(c *contextmodel.ReqContext, dashID int64) (bool, error) { if !c.IsSignedIn { return false, nil } @@ -63,7 +63,7 @@ func dashboardGuardianResponse(err error) response.Response { // 200: trimDashboardResponse // 401: unauthorisedError // 500: internalServerError -func (hs *HTTPServer) TrimDashboard(c *models.ReqContext) response.Response { +func (hs *HTTPServer) TrimDashboard(c *contextmodel.ReqContext) response.Response { cmd := dashboards.TrimDashboardCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -93,7 +93,7 @@ func (hs *HTTPServer) TrimDashboard(c *models.ReqContext) response.Response { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetDashboard(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetDashboard(c *contextmodel.ReqContext) response.Response { uid := web.Params(c.Req)[":uid"] dash, rsp := hs.getDashboardHelper(c.Req.Context(), c.OrgID, 0, uid) if rsp != nil { @@ -242,7 +242,7 @@ func (hs *HTTPServer) GetDashboard(c *models.ReqContext) response.Response { return response.JSON(http.StatusOK, dto) } -func (hs *HTTPServer) getAnnotationPermissionsByScope(c *models.ReqContext, actions *dtos.AnnotationActions, scope string) { +func (hs *HTTPServer) getAnnotationPermissionsByScope(c *contextmodel.ReqContext, actions *dtos.AnnotationActions, scope string) { var err error evaluate := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsCreate, scope) @@ -302,11 +302,11 @@ func (hs *HTTPServer) getDashboardHelper(ctx context.Context, orgID int64, id in // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) DeleteDashboardByUID(c *models.ReqContext) response.Response { +func (hs *HTTPServer) DeleteDashboardByUID(c *contextmodel.ReqContext) response.Response { return hs.deleteDashboard(c) } -func (hs *HTTPServer) deleteDashboard(c *models.ReqContext) response.Response { +func (hs *HTTPServer) deleteDashboard(c *contextmodel.ReqContext) response.Response { dash, rsp := hs.getDashboardHelper(c.Req.Context(), c.OrgID, 0, web.Params(c.Req)[":uid"]) if rsp != nil { return rsp @@ -365,7 +365,7 @@ func (hs *HTTPServer) deleteDashboard(c *models.ReqContext) response.Response { // 412: preconditionFailedError // 422: unprocessableEntityError // 500: internalServerError -func (hs *HTTPServer) PostDashboard(c *models.ReqContext) response.Response { +func (hs *HTTPServer) PostDashboard(c *contextmodel.ReqContext) response.Response { cmd := dashboards.SaveDashboardCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -396,7 +396,7 @@ func (hs *HTTPServer) PostDashboard(c *models.ReqContext) response.Response { return hs.postDashboard(c, cmd) } -func (hs *HTTPServer) postDashboard(c *models.ReqContext, cmd dashboards.SaveDashboardCommand) response.Response { +func (hs *HTTPServer) postDashboard(c *contextmodel.ReqContext, cmd dashboards.SaveDashboardCommand) response.Response { ctx := c.Req.Context() var err error cmd.OrgID = c.OrgID @@ -517,7 +517,7 @@ func (hs *HTTPServer) postDashboard(c *models.ReqContext, cmd dashboards.SaveDas // 200: getHomeDashboardResponse // 401: unauthorisedError // 500: internalServerError -func (hs *HTTPServer) GetHomeDashboard(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetHomeDashboard(c *contextmodel.ReqContext) response.Response { prefsQuery := pref.GetPreferenceWithDefaultsQuery{OrgID: c.OrgID, UserID: c.SignedInUser.UserID, Teams: c.Teams} homePage := hs.Cfg.HomePage @@ -575,7 +575,7 @@ func (hs *HTTPServer) GetHomeDashboard(c *models.ReqContext) response.Response { return response.JSON(http.StatusOK, &dash) } -func (hs *HTTPServer) addGettingStartedPanelToHomeDashboard(c *models.ReqContext, dash *simplejson.Json) { +func (hs *HTTPServer) addGettingStartedPanelToHomeDashboard(c *contextmodel.ReqContext, dash *simplejson.Json) { // We only add this getting started panel for Admins who have not dismissed it, // and if a custom default home dashboard hasn't been configured if !c.HasUserRole(org.RoleAdmin) || @@ -626,7 +626,7 @@ func (hs *HTTPServer) addGettingStartedPanelToHomeDashboard(c *models.ReqContext // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetDashboardVersions(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetDashboardVersions(c *contextmodel.ReqContext) response.Response { var dashID int64 var err error @@ -709,7 +709,7 @@ func (hs *HTTPServer) GetDashboardVersions(c *models.ReqContext) response.Respon // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetDashboardVersion(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetDashboardVersion(c *contextmodel.ReqContext) response.Response { var dashID int64 var err error @@ -784,7 +784,7 @@ func (hs *HTTPServer) GetDashboardVersion(c *models.ReqContext) response.Respons // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) ValidateDashboard(c *models.ReqContext) response.Response { +func (hs *HTTPServer) ValidateDashboard(c *contextmodel.ReqContext) response.Response { cmd := dashboards.ValidateDashboardCommand{} if err := web.Bind(c.Req, &cmd); err != nil { @@ -846,7 +846,7 @@ func (hs *HTTPServer) ValidateDashboard(c *models.ReqContext) response.Response // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) CalculateDashboardDiff(c *models.ReqContext) response.Response { +func (hs *HTTPServer) CalculateDashboardDiff(c *contextmodel.ReqContext) response.Response { apiOptions := dtos.CalculateDiffOptions{} if err := web.Bind(c.Req, &apiOptions); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -958,7 +958,7 @@ func (hs *HTTPServer) CalculateDashboardDiff(c *models.ReqContext) response.Resp // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) RestoreDashboardVersion(c *models.ReqContext) response.Response { +func (hs *HTTPServer) RestoreDashboardVersion(c *contextmodel.ReqContext) response.Response { var dashID int64 var err error @@ -1016,7 +1016,7 @@ func (hs *HTTPServer) RestoreDashboardVersion(c *models.ReqContext) response.Res // 200: getDashboardsTagsResponse // 401: unauthorisedError // 500: internalServerError -func (hs *HTTPServer) GetDashboardTags(c *models.ReqContext) { +func (hs *HTTPServer) GetDashboardTags(c *contextmodel.ReqContext) { query := dashboards.GetDashboardTagsQuery{OrgID: c.OrgID} queryResult, err := hs.DashboardService.GetDashboardTags(c.Req.Context(), &query) if err != nil { @@ -1028,7 +1028,7 @@ func (hs *HTTPServer) GetDashboardTags(c *models.ReqContext) { } // GetDashboardUIDs converts internal ids to UIDs -func (hs *HTTPServer) GetDashboardUIDs(c *models.ReqContext) { +func (hs *HTTPServer) GetDashboardUIDs(c *contextmodel.ReqContext) { ids := strings.Split(web.Params(c.Req)[":ids"], ",") uids := make([]string, 0, len(ids)) diff --git a/pkg/api/dashboard_permission.go b/pkg/api/dashboard_permission.go index 6548547ee14..9f83b6e7579 100644 --- a/pkg/api/dashboard_permission.go +++ b/pkg/api/dashboard_permission.go @@ -9,8 +9,8 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/web" @@ -41,7 +41,7 @@ import ( // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetDashboardPermissionList(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetDashboardPermissionList(c *contextmodel.ReqContext) response.Response { var dashID int64 var err error dashUID := web.Params(c.Req)[":uid"] @@ -123,7 +123,7 @@ func (hs *HTTPServer) GetDashboardPermissionList(c *models.ReqContext) response. // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) UpdateDashboardPermissions(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UpdateDashboardPermissions(c *contextmodel.ReqContext) response.Response { var dashID int64 var err error apiCmd := dtos.UpdateDashboardACLCommand{} diff --git a/pkg/api/dashboard_permission_test.go b/pkg/api/dashboard_permission_test.go index 4ae44e3a02e..0d2955b58a2 100644 --- a/pkg/api/dashboard_permission_test.go +++ b/pkg/api/dashboard_permission_test.go @@ -15,8 +15,8 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/db/dbtest" "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/models" accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" dashboardservice "github.com/grafana/grafana/pkg/services/dashboards/service" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -40,7 +40,7 @@ func TestDashboardPermissionAPIEndpoint(t *testing.T) { folderPermissions := accesscontrolmock.NewMockedPermissionsService() dashboardPermissions := accesscontrolmock.NewMockedPermissionsService() - folderSvc := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), settings, dashboardStore, dashboards.NewFakeFolderStore(t), mockSQLStore, featuremgmt.WithFeatures(), nil) + folderSvc := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), settings, dashboardStore, dashboards.NewFakeFolderStore(t), mockSQLStore, featuremgmt.WithFeatures()) hs := &HTTPServer{ Cfg: settings, SQLStore: mockSQLStore, @@ -354,7 +354,7 @@ func updateDashboardPermissionScenario(t *testing.T, ctx updatePermissionContext t.Run(fmt.Sprintf("%s %s", ctx.desc, ctx.url), func(t *testing.T) { sc := setupScenarioContext(t, ctx.url) - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(ctx.cmd) c.Req.Header.Add("Content-Type", "application/json") sc.context = c diff --git a/pkg/api/dashboard_snapshot.go b/pkg/api/dashboard_snapshot.go index 5d017a82838..a367c69dd5c 100644 --- a/pkg/api/dashboard_snapshot.go +++ b/pkg/api/dashboard_snapshot.go @@ -12,7 +12,7 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/metrics" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboardsnapshots" "github.com/grafana/grafana/pkg/services/guardian" @@ -33,7 +33,7 @@ var client = &http.Client{ // Responses: // 200: getSharingOptionsResponse // 401: unauthorisedError -func (hs *HTTPServer) GetSharingOptions(c *models.ReqContext) { +func (hs *HTTPServer) GetSharingOptions(c *contextmodel.ReqContext) { c.JSON(http.StatusOK, util.DynMap{ "snapshotEnabled": hs.Cfg.SnapshotEnabled, "externalSnapshotURL": hs.Cfg.ExternalSnapshotUrl, @@ -105,7 +105,7 @@ func createOriginalDashboardURL(cmd *dashboardsnapshots.CreateDashboardSnapshotC // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) CreateDashboardSnapshot(c *models.ReqContext) response.Response { +func (hs *HTTPServer) CreateDashboardSnapshot(c *contextmodel.ReqContext) response.Response { if !hs.Cfg.SnapshotEnabled { c.JsonApiErr(http.StatusForbidden, "Dashboard Snapshots are disabled", nil) return nil @@ -200,7 +200,7 @@ func (hs *HTTPServer) CreateDashboardSnapshot(c *models.ReqContext) response.Res // 400: badRequestError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetDashboardSnapshot(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetDashboardSnapshot(c *contextmodel.ReqContext) response.Response { if !hs.Cfg.SnapshotEnabled { c.JsonApiErr(http.StatusForbidden, "Dashboard Snapshots are disabled", nil) return nil @@ -284,7 +284,7 @@ func deleteExternalDashboardSnapshot(externalUrl string) error { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) DeleteDashboardSnapshotByDeleteKey(c *models.ReqContext) response.Response { +func (hs *HTTPServer) DeleteDashboardSnapshotByDeleteKey(c *contextmodel.ReqContext) response.Response { if !hs.Cfg.SnapshotEnabled { c.JsonApiErr(http.StatusForbidden, "Dashboard Snapshots are disabled", nil) return nil @@ -329,7 +329,7 @@ func (hs *HTTPServer) DeleteDashboardSnapshotByDeleteKey(c *models.ReqContext) r // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) DeleteDashboardSnapshot(c *models.ReqContext) response.Response { +func (hs *HTTPServer) DeleteDashboardSnapshot(c *contextmodel.ReqContext) response.Response { if !hs.Cfg.SnapshotEnabled { c.JsonApiErr(http.StatusForbidden, "Dashboard Snapshots are disabled", nil) return nil @@ -399,7 +399,7 @@ func (hs *HTTPServer) DeleteDashboardSnapshot(c *models.ReqContext) response.Res // Responses: // 200: searchDashboardSnapshotsResponse // 500: internalServerError -func (hs *HTTPServer) SearchDashboardSnapshots(c *models.ReqContext) response.Response { +func (hs *HTTPServer) SearchDashboardSnapshots(c *contextmodel.ReqContext) response.Response { if !hs.Cfg.SnapshotEnabled { c.JsonApiErr(http.StatusForbidden, "Dashboard Snapshots are disabled", nil) return nil diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 0623d413707..e5bbf001564 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -22,7 +22,6 @@ import ( "github.com/grafana/grafana/pkg/infra/db/dbtest" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/infra/usagestats" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/registry/corekind" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" @@ -30,6 +29,7 @@ import ( accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/annotations/annotationstest" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards/database" "github.com/grafana/grafana/pkg/services/dashboards/service" @@ -58,7 +58,7 @@ func TestGetHomeDashboard(t *testing.T) { httpReq, err := http.NewRequest(http.MethodGet, "", nil) require.NoError(t, err) httpReq.Header.Add("Content-Type", "application/json") - req := &models.ReqContext{SignedInUser: &user.SignedInUser{}, Context: &web.Context{Req: httpReq}} + req := &contextmodel.ReqContext{SignedInUser: &user.SignedInUser{}, Context: &web.Context{Req: httpReq}} cfg := setting.NewCfg() cfg.StaticRootPath = "../../public/" prefService := preftest.NewPreferenceServiceFake() @@ -983,7 +983,7 @@ func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, pr dashboardPermissions := accesscontrolmock.NewMockedPermissionsService() features := featuremgmt.WithFeatures() - folderSvc := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), cfg, dashboardStore, folderStore, db.InitTestDB(t), featuremgmt.WithFeatures(), nil) + folderSvc := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), cfg, dashboardStore, folderStore, db.InitTestDB(t), featuremgmt.WithFeatures()) if dashboardService == nil { dashboardService = service.ProvideDashboardService( @@ -1076,7 +1076,7 @@ func postDashboardScenario(t *testing.T, desc string, url string, routePattern s } sc := setupScenarioContext(t, url) - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(cmd) c.Req.Header.Add("Content-Type", "application/json") sc.context = c @@ -1108,7 +1108,7 @@ func postValidateScenario(t *testing.T, desc string, url string, routePattern st } sc := setupScenarioContext(t, url) - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(cmd) c.Req.Header.Add("Content-Type", "application/json") sc.context = c @@ -1148,7 +1148,7 @@ func postDiffScenario(t *testing.T, desc string, url string, routePattern string } sc := setupScenarioContext(t, url) - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(cmd) c.Req.Header.Add("Content-Type", "application/json") sc.context = c @@ -1190,7 +1190,7 @@ func restoreDashboardVersionScenario(t *testing.T, desc string, url string, rout sc := setupScenarioContext(t, url) sc.sqlStore = sqlStore sc.dashboardVersionService = fakeDashboardVersionService - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(cmd) c.Req.Header.Add("Content-Type", "application/json") sc.context = c diff --git a/pkg/api/dataproxy.go b/pkg/api/dataproxy.go index 09edcfd379a..b7be8b84979 100644 --- a/pkg/api/dataproxy.go +++ b/pkg/api/dataproxy.go @@ -1,6 +1,6 @@ package api -import "github.com/grafana/grafana/pkg/models" +import contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" // swagger:route GET /datasources/proxy/{id}/{datasource_proxy_route} datasources datasourceProxyGETcalls // @@ -56,7 +56,7 @@ import "github.com/grafana/grafana/pkg/models" // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) ProxyDataSourceRequest(c *models.ReqContext) { +func (hs *HTTPServer) ProxyDataSourceRequest(c *contextmodel.ReqContext) { hs.DataProxy.ProxyDataSourceRequest(c) } @@ -102,7 +102,7 @@ func (hs *HTTPServer) ProxyDataSourceRequest(c *models.ReqContext) { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) ProxyDataSourceRequestWithUID(c *models.ReqContext) { +func (hs *HTTPServer) ProxyDataSourceRequestWithUID(c *contextmodel.ReqContext) { hs.DataProxy.ProxyDatasourceRequestWithUID(c, "") } diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index 7e2ff3f32ec..b2efa244fe3 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -17,8 +17,8 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins/adapters" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/datasources/permissions" "github.com/grafana/grafana/pkg/services/user" @@ -42,7 +42,7 @@ var secretsPluginError datasources.ErrDatasourceSecretsPluginUserFriendly // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) GetDataSources(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetDataSources(c *contextmodel.ReqContext) response.Response { query := datasources.GetDataSourcesQuery{OrgId: c.OrgID, DataSourceLimit: hs.Cfg.DataSourceLimit} if err := hs.DataSourcesService.GetDataSources(c.Req.Context(), &query); err != nil { @@ -106,7 +106,7 @@ func (hs *HTTPServer) GetDataSources(c *models.ReqContext) response.Response { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetDataSourceById(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetDataSourceById(c *contextmodel.ReqContext) response.Response { id, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "id is invalid", nil) @@ -151,7 +151,7 @@ func (hs *HTTPServer) GetDataSourceById(c *models.ReqContext) response.Response // 404: notFoundError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) DeleteDataSourceById(c *models.ReqContext) response.Response { +func (hs *HTTPServer) DeleteDataSourceById(c *contextmodel.ReqContext) response.Response { id, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "id is invalid", err) @@ -202,7 +202,7 @@ func (hs *HTTPServer) DeleteDataSourceById(c *models.ReqContext) response.Respon // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetDataSourceByUID(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetDataSourceByUID(c *contextmodel.ReqContext) response.Response { ds, err := hs.getRawDataSourceByUID(c.Req.Context(), web.Params(c.Req)[":uid"], c.OrgID) if err != nil { @@ -233,7 +233,7 @@ func (hs *HTTPServer) GetDataSourceByUID(c *models.ReqContext) response.Response // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) DeleteDataSourceByUID(c *models.ReqContext) response.Response { +func (hs *HTTPServer) DeleteDataSourceByUID(c *contextmodel.ReqContext) response.Response { uid := web.Params(c.Req)[":uid"] if uid == "" { @@ -283,7 +283,7 @@ func (hs *HTTPServer) DeleteDataSourceByUID(c *models.ReqContext) response.Respo // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) DeleteDataSourceByName(c *models.ReqContext) response.Response { +func (hs *HTTPServer) DeleteDataSourceByName(c *contextmodel.ReqContext) response.Response { name := web.Params(c.Req)[":name"] if name == "" { @@ -365,7 +365,7 @@ func validateJSONData(jsonData *simplejson.Json, cfg *setting.Cfg) error { // 403: forbiddenError // 409: conflictError // 500: internalServerError -func (hs *HTTPServer) AddDataSource(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AddDataSource(c *contextmodel.ReqContext) response.Response { cmd := datasources.AddDataSourceCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -431,7 +431,7 @@ func (hs *HTTPServer) AddDataSource(c *models.ReqContext) response.Response { // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) UpdateDataSourceByID(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UpdateDataSourceByID(c *contextmodel.ReqContext) response.Response { cmd := datasources.UpdateDataSourceCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -475,7 +475,7 @@ func (hs *HTTPServer) UpdateDataSourceByID(c *models.ReqContext) response.Respon // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) UpdateDataSourceByUID(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UpdateDataSourceByUID(c *contextmodel.ReqContext) response.Response { cmd := datasources.UpdateDataSourceCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -500,7 +500,7 @@ func (hs *HTTPServer) UpdateDataSourceByUID(c *models.ReqContext) response.Respo return hs.updateDataSourceByID(c, ds, cmd) } -func (hs *HTTPServer) updateDataSourceByID(c *models.ReqContext, ds *datasources.DataSource, cmd datasources.UpdateDataSourceCommand) response.Response { +func (hs *HTTPServer) updateDataSourceByID(c *contextmodel.ReqContext, ds *datasources.DataSource, cmd datasources.UpdateDataSourceCommand) response.Response { if ds.ReadOnly { return response.Error(403, "Cannot update read-only data source", nil) } @@ -579,7 +579,7 @@ func (hs *HTTPServer) getRawDataSourceByUID(ctx context.Context, uid string, org // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) GetDataSourceByName(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetDataSourceByName(c *contextmodel.ReqContext) response.Response { query := datasources.GetDataSourceQuery{Name: web.Params(c.Req)[":name"], OrgId: c.OrgID} if err := hs.DataSourcesService.GetDataSource(c.Req.Context(), &query); err != nil { @@ -606,7 +606,7 @@ func (hs *HTTPServer) GetDataSourceByName(c *models.ReqContext) response.Respons // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetDataSourceIdByName(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetDataSourceIdByName(c *contextmodel.ReqContext) response.Response { query := datasources.GetDataSourceQuery{Name: web.Params(c.Req)[":name"], OrgId: c.OrgID} if err := hs.DataSourcesService.GetDataSource(c.Req.Context(), &query); err != nil { @@ -639,7 +639,7 @@ func (hs *HTTPServer) GetDataSourceIdByName(c *models.ReqContext) response.Respo // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) CallDatasourceResource(c *models.ReqContext) { +func (hs *HTTPServer) CallDatasourceResource(c *contextmodel.ReqContext) { datasourceID, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64) if err != nil { c.JsonApiErr(http.StatusBadRequest, "id is invalid", nil) @@ -675,7 +675,7 @@ func (hs *HTTPServer) CallDatasourceResource(c *models.ReqContext) { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) CallDatasourceResourceWithUID(c *models.ReqContext) { +func (hs *HTTPServer) CallDatasourceResourceWithUID(c *contextmodel.ReqContext) { dsUID := web.Params(c.Req)[":uid"] if !util.IsValidShortUID(dsUID) { c.JsonApiErr(http.StatusBadRequest, "UID is invalid", nil) @@ -746,7 +746,7 @@ func (hs *HTTPServer) convertModelToDtos(ctx context.Context, ds *datasources.Da // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) CheckDatasourceHealthWithUID(c *models.ReqContext) response.Response { +func (hs *HTTPServer) CheckDatasourceHealthWithUID(c *contextmodel.ReqContext) response.Response { dsUID := web.Params(c.Req)[":uid"] if !util.IsValidShortUID(dsUID) { return response.Error(http.StatusBadRequest, "UID is invalid", nil) @@ -776,7 +776,7 @@ func (hs *HTTPServer) CheckDatasourceHealthWithUID(c *models.ReqContext) respons // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) CheckDatasourceHealth(c *models.ReqContext) response.Response { +func (hs *HTTPServer) CheckDatasourceHealth(c *contextmodel.ReqContext) response.Response { datasourceID, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "id is invalid", nil) @@ -792,7 +792,7 @@ func (hs *HTTPServer) CheckDatasourceHealth(c *models.ReqContext) response.Respo return hs.checkDatasourceHealth(c, ds) } -func (hs *HTTPServer) checkDatasourceHealth(c *models.ReqContext, ds *datasources.DataSource) response.Response { +func (hs *HTTPServer) checkDatasourceHealth(c *contextmodel.ReqContext, ds *datasources.DataSource) response.Response { plugin, exists := hs.pluginStore.Plugin(c.Req.Context(), ds.Type) if !exists { return response.Error(http.StatusInternalServerError, "Unable to find datasource plugin", nil) diff --git a/pkg/api/datasources_test.go b/pkg/api/datasources_test.go index f9b313c6b5a..8eabf669b34 100644 --- a/pkg/api/datasources_test.go +++ b/pkg/api/datasources_test.go @@ -8,7 +8,6 @@ import ( "strings" "testing" - "github.com/grafana/grafana/pkg/web/webtest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -16,14 +15,15 @@ import ( "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db/dbtest" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/datasources/permissions" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/web/webtest" ) const ( @@ -88,7 +88,7 @@ func TestAddDataSource_InvalidURL(t *testing.T) { Cfg: setting.NewCfg(), } - sc.m.Post(sc.url, routing.Wrap(func(c *models.ReqContext) response.Response { + sc.m.Post(sc.url, routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(datasources.AddDataSourceCommand{ Name: "Test", Url: "invalid:url", @@ -119,7 +119,7 @@ func TestAddDataSource_URLWithoutProtocol(t *testing.T) { sc := setupScenarioContext(t, "/api/datasources") - sc.m.Post(sc.url, routing.Wrap(func(c *models.ReqContext) response.Response { + sc.m.Post(sc.url, routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(datasources.AddDataSourceCommand{ Name: name, Url: url, @@ -149,7 +149,7 @@ func TestAddDataSource_InvalidJSONData(t *testing.T) { jsonData := simplejson.New() jsonData.Set("httpHeaderName1", hs.Cfg.AuthProxyHeaderName) - sc.m.Post(sc.url, routing.Wrap(func(c *models.ReqContext) response.Response { + sc.m.Post(sc.url, routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(datasources.AddDataSourceCommand{ Name: "Test", Url: "localhost:5432", @@ -173,7 +173,7 @@ func TestUpdateDataSource_InvalidURL(t *testing.T) { } sc := setupScenarioContext(t, "/api/datasources/1234") - sc.m.Put(sc.url, routing.Wrap(func(c *models.ReqContext) response.Response { + sc.m.Put(sc.url, routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(datasources.AddDataSourceCommand{ Name: "Test", Url: "invalid:url", @@ -201,7 +201,7 @@ func TestUpdateDataSource_InvalidJSONData(t *testing.T) { jsonData := simplejson.New() jsonData.Set("httpHeaderName1", hs.Cfg.AuthProxyHeaderName) - sc.m.Put(sc.url, routing.Wrap(func(c *models.ReqContext) response.Response { + sc.m.Put(sc.url, routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(datasources.AddDataSourceCommand{ Name: "Test", Url: "localhost:5432", @@ -233,7 +233,7 @@ func TestUpdateDataSource_URLWithoutProtocol(t *testing.T) { sc := setupScenarioContext(t, "/api/datasources/1234") - sc.m.Put(sc.url, routing.Wrap(func(c *models.ReqContext) response.Response { + sc.m.Put(sc.url, routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(datasources.AddDataSourceCommand{ Name: name, Url: url, diff --git a/pkg/api/dtos/index.go b/pkg/api/dtos/index.go index 6d1263d0c1b..049a13f0425 100644 --- a/pkg/api/dtos/index.go +++ b/pkg/api/dtos/index.go @@ -1,10 +1,10 @@ package dtos import ( + "html/template" + "github.com/grafana/grafana/pkg/services/navtree" "github.com/grafana/grafana/pkg/setting" - - "html/template" ) type IndexViewData struct { diff --git a/pkg/api/dtos/models.go b/pkg/api/dtos/models.go index 2b65fa0e4b5..9394ca0c877 100644 --- a/pkg/api/dtos/models.go +++ b/pkg/api/dtos/models.go @@ -33,7 +33,8 @@ type CurrentUser struct { Login string `json:"login"` Email string `json:"email"` Name string `json:"name"` - LightTheme bool `json:"lightTheme"` + Theme string `json:"theme"` + LightTheme bool `json:"lightTheme"` // deprecated, use theme instead OrgCount int `json:"orgCount"` OrgId int64 `json:"orgId"` OrgName string `json:"orgName"` diff --git a/pkg/api/dtos/models_test.go b/pkg/api/dtos/models_test.go index 931676f5a04..39dd133f2fe 100644 --- a/pkg/api/dtos/models_test.go +++ b/pkg/api/dtos/models_test.go @@ -4,10 +4,11 @@ import ( "sort" "testing" + "github.com/stretchr/testify/assert" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/assert" ) func TestGetUniqueDatasourceTypes(t *testing.T) { diff --git a/pkg/api/dtos/prefs.go b/pkg/api/dtos/prefs.go index b92ae22f221..c8f80e40cc8 100644 --- a/pkg/api/dtos/prefs.go +++ b/pkg/api/dtos/prefs.go @@ -6,7 +6,7 @@ import ( // swagger:model type UpdatePrefsCmd struct { - // Enum: light,dark + // Enum: light,dark,system Theme string `json:"theme"` // The numerical :id of a favorited dashboard // Default:0 diff --git a/pkg/api/folder.go b/pkg/api/folder.go index 20cede8a574..a2f483450d2 100644 --- a/pkg/api/folder.go +++ b/pkg/api/folder.go @@ -1,6 +1,7 @@ package api import ( + "context" "errors" "net/http" "strconv" @@ -8,13 +9,16 @@ import ( "github.com/grafana/grafana/pkg/api/apierrors" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/libraryelements" + "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/search" + "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/web" ) @@ -31,7 +35,7 @@ import ( // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) GetFolders(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetFolders(c *contextmodel.ReqContext) response.Response { var folders []*folder.Folder var err error if hs.Features.IsEnabled(featuremgmt.FlagNestedFolders) { @@ -82,7 +86,7 @@ func (hs *HTTPServer) GetFolders(c *models.ReqContext) response.Response { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetFolderByUID(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetFolderByUID(c *contextmodel.ReqContext) response.Response { uid := web.Params(c.Req)[":uid"] folder, err := hs.folderService.Get(c.Req.Context(), &folder.GetFolderQuery{OrgID: c.OrgID, UID: &uid, SignedInUser: c.SignedInUser}) if err != nil { @@ -109,7 +113,7 @@ func (hs *HTTPServer) GetFolderByUID(c *models.ReqContext) response.Response { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetFolderByID(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetFolderByID(c *contextmodel.ReqContext) response.Response { id, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "id is invalid", err) @@ -139,7 +143,7 @@ func (hs *HTTPServer) GetFolderByID(c *models.ReqContext) response.Response { // 403: forbiddenError // 409: conflictError // 500: internalServerError -func (hs *HTTPServer) CreateFolder(c *models.ReqContext) response.Response { +func (hs *HTTPServer) CreateFolder(c *contextmodel.ReqContext) response.Response { cmd := folder.CreateFolderCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -152,6 +156,10 @@ func (hs *HTTPServer) CreateFolder(c *models.ReqContext) response.Response { return apierrors.ToFolderErrorResponse(err) } + if err := hs.setDefaultFolderPermissions(c.Req.Context(), cmd.OrgID, cmd.SignedInUser, folder); err != nil { + hs.log.Error("Could not set the default folder permissions", "folder", folder.Title, "user", cmd.SignedInUser, "error", err) + } + // Clear permission cache for the user who's created the folder, so that new permissions are fetched for their next call // Required for cases when caller wants to immediately interact with the newly created object if !hs.AccessControl.IsDisabled() { @@ -167,7 +175,31 @@ func (hs *HTTPServer) CreateFolder(c *models.ReqContext) response.Response { return response.JSON(http.StatusOK, hs.newToFolderDto(c, g, folder)) } -func (hs *HTTPServer) MoveFolder(c *models.ReqContext) response.Response { +func (hs *HTTPServer) setDefaultFolderPermissions(ctx context.Context, orgID int64, user *user.SignedInUser, folder *folder.Folder) error { + // Set default folder permissions + var permissionErr error + if !accesscontrol.IsDisabled(hs.Cfg) { + var permissions []accesscontrol.SetResourcePermissionCommand + if user.IsRealUser() && !user.IsAnonymous { + permissions = append(permissions, accesscontrol.SetResourcePermissionCommand{ + UserID: user.UserID, Permission: dashboards.PERMISSION_ADMIN.String(), + }) + } + + permissions = append(permissions, []accesscontrol.SetResourcePermissionCommand{ + {BuiltinRole: string(org.RoleEditor), Permission: dashboards.PERMISSION_EDIT.String()}, + {BuiltinRole: string(org.RoleViewer), Permission: dashboards.PERMISSION_VIEW.String()}, + }...) + + _, permissionErr = hs.folderPermissionsService.SetPermissions(ctx, orgID, folder.UID, permissions...) + return permissionErr + } else if hs.Cfg.EditorsCanAdmin && user.IsRealUser() && !user.IsAnonymous { + return hs.folderService.MakeUserAdmin(ctx, orgID, user.UserID, folder.ID, true) + } + return nil +} + +func (hs *HTTPServer) MoveFolder(c *contextmodel.ReqContext) response.Response { if hs.Features.IsEnabled(featuremgmt.FlagNestedFolders) { cmd := folder.MoveFolderCommand{} if err := web.Bind(c.Req, &cmd); err != nil { @@ -205,7 +237,7 @@ func (hs *HTTPServer) MoveFolder(c *models.ReqContext) response.Response { // 404: notFoundError // 409: conflictError // 500: internalServerError -func (hs *HTTPServer) UpdateFolder(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UpdateFolder(c *contextmodel.ReqContext) response.Response { cmd := folder.UpdateFolderCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -240,7 +272,7 @@ func (hs *HTTPServer) UpdateFolder(c *models.ReqContext) response.Response { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) DeleteFolder(c *models.ReqContext) response.Response { // temporarily adding this function to HTTPServer, will be removed from HTTPServer when librarypanels featuretoggle is removed +func (hs *HTTPServer) DeleteFolder(c *contextmodel.ReqContext) response.Response { // temporarily adding this function to HTTPServer, will be removed from HTTPServer when librarypanels featuretoggle is removed err := hs.LibraryElementService.DeleteLibraryElementsInFolder(c.Req.Context(), c.SignedInUser, web.Params(c.Req)[":uid"]) if err != nil { if errors.Is(err, libraryelements.ErrFolderHasConnectedLibraryElements) { @@ -258,7 +290,7 @@ func (hs *HTTPServer) DeleteFolder(c *models.ReqContext) response.Response { // return response.JSON(http.StatusOK, "") } -func (hs *HTTPServer) newToFolderDto(c *models.ReqContext, g guardian.DashboardGuardian, folder *folder.Folder) dtos.Folder { +func (hs *HTTPServer) newToFolderDto(c *contextmodel.ReqContext, g guardian.DashboardGuardian, folder *folder.Folder) dtos.Folder { canEdit, _ := g.CanEdit() canSave, _ := g.CanSave() canAdmin, _ := g.CanAdmin() @@ -293,7 +325,7 @@ func (hs *HTTPServer) newToFolderDto(c *models.ReqContext, g guardian.DashboardG } } -func (hs *HTTPServer) searchFolders(c *models.ReqContext) ([]*folder.Folder, error) { +func (hs *HTTPServer) searchFolders(c *contextmodel.ReqContext) ([]*folder.Folder, error) { searchQuery := search.Query{ SignedInUser: c.SignedInUser, DashboardIds: make([]int64, 0), diff --git a/pkg/api/folder_permission.go b/pkg/api/folder_permission.go index 530d0a4fe17..d7916f4bb0e 100644 --- a/pkg/api/folder_permission.go +++ b/pkg/api/folder_permission.go @@ -8,7 +8,7 @@ import ( "github.com/grafana/grafana/pkg/api/apierrors" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/guardian" @@ -26,7 +26,7 @@ import ( // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetFolderPermissionList(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetFolderPermissionList(c *contextmodel.ReqContext) response.Response { uid := web.Params(c.Req)[":uid"] folder, err := hs.folderService.Get(c.Req.Context(), &folder.GetFolderQuery{OrgID: c.OrgID, UID: &uid, SignedInUser: c.SignedInUser}) @@ -83,7 +83,7 @@ func (hs *HTTPServer) GetFolderPermissionList(c *models.ReqContext) response.Res // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) UpdateFolderPermissions(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UpdateFolderPermissions(c *contextmodel.ReqContext) response.Response { apiCmd := dtos.UpdateDashboardACLCommand{} if err := web.Bind(c.Req, &apiCmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) diff --git a/pkg/api/folder_permission_test.go b/pkg/api/folder_permission_test.go index 9f30eb92f24..03eeed7abe8 100644 --- a/pkg/api/folder_permission_test.go +++ b/pkg/api/folder_permission_test.go @@ -13,8 +13,8 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/infra/db/dbtest" - "github.com/grafana/grafana/pkg/models" accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" service "github.com/grafana/grafana/pkg/services/dashboards/service" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -361,7 +361,7 @@ func updateFolderPermissionScenario(t *testing.T, ctx updatePermissionContext, h t.Run(fmt.Sprintf("%s %s", ctx.desc, ctx.url), func(t *testing.T) { sc := setupScenarioContext(t, ctx.url) - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(ctx.cmd) c.Req.Header.Add("Content-Type", "application/json") sc.context = c diff --git a/pkg/api/folder_test.go b/pkg/api/folder_test.go index 3a8f201560f..f5a56a1f74b 100644 --- a/pkg/api/folder_test.go +++ b/pkg/api/folder_test.go @@ -14,16 +14,17 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/infra/db/dbtest" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/folder/foldertest" "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/quota/quotatest" + "github.com/grafana/grafana/pkg/services/search/model" "github.com/grafana/grafana/pkg/services/team/teamtest" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" @@ -150,7 +151,7 @@ func TestHTTPServer_FolderMetadata(t *testing.T) { hs.AccessControl = acmock.New() hs.QuotaService = quotatest.New(false, nil) hs.SearchService = &mockSearchService{ - ExpectedResult: models.HitList{}, + ExpectedResult: model.HitList{}, } }) @@ -247,16 +248,19 @@ func createFolderScenario(t *testing.T, desc string, url string, routePattern st dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Return(qResult, nil) store := dbtest.NewFakeDB() guardian.InitLegacyGuardian(store, dashSvc, teamSvc) + folderPermissions := acmock.NewMockedPermissionsService() + folderPermissions.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil) hs := HTTPServer{ - AccessControl: acmock.New(), - folderService: folderService, - Cfg: setting.NewCfg(), - Features: featuremgmt.WithFeatures(), - accesscontrolService: actest.FakeService{}, + AccessControl: acmock.New(), + folderService: folderService, + Cfg: setting.NewCfg(), + Features: featuremgmt.WithFeatures(), + accesscontrolService: actest.FakeService{}, + folderPermissionsService: folderPermissions, } sc := setupScenarioContext(t, url) - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(cmd) c.Req.Header.Add("Content-Type", "application/json") sc.context = c @@ -286,7 +290,7 @@ func updateFolderScenario(t *testing.T, desc string, url string, routePattern st } sc := setupScenarioContext(t, url) - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(cmd) c.Req.Header.Add("Content-Type", "application/json") sc.context = c diff --git a/pkg/api/frontend_logging_test.go b/pkg/api/frontend_logging_test.go index 81f64a50223..5771bb4465c 100644 --- a/pkg/api/frontend_logging_test.go +++ b/pkg/api/frontend_logging_test.go @@ -12,15 +12,15 @@ import ( "github.com/getsentry/sentry-go" "github.com/go-kit/log" "github.com/go-kit/log/level" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/api/frontendlogging" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/setting" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) type SourceMapReadRecord struct { @@ -89,7 +89,7 @@ func logSentryEventScenario(t *testing.T, desc string, event frontendlogging.Fro loggingHandler := NewFrontendLogMessageHandler(sourceMapStore) - handler := routing.Wrap(func(c *models.ReqContext) response.Response { + handler := routing.Wrap(func(c *contextmodel.ReqContext) response.Response { sc.context = c c.Req.Body = mockRequestBody(event) c.Req.Header.Add("Content-Type", "application/json") @@ -162,7 +162,7 @@ func logGrafanaJavascriptAgentEventScenario(t *testing.T, desc string, event fro loggingHandler := GrafanaJavascriptAgentLogMessageHandler(sourceMapStore) - handler := routing.Wrap(func(c *models.ReqContext) response.Response { + handler := routing.Wrap(func(c *contextmodel.ReqContext) response.Response { sc.context = c c.Req.Body = mockRequestBody(event) c.Req.Header.Add("Content-Type", "application/json") diff --git a/pkg/api/frontend_metrics.go b/pkg/api/frontend_metrics.go index 6c6901786d2..bb5f35e0ce3 100644 --- a/pkg/api/frontend_metrics.go +++ b/pkg/api/frontend_metrics.go @@ -5,11 +5,11 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/metrics" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/web" ) -func (hs *HTTPServer) PostFrontendMetrics(c *models.ReqContext) response.Response { +func (hs *HTTPServer) PostFrontendMetrics(c *contextmodel.ReqContext) response.Response { cmd := metrics.PostFrontendMetricsCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) diff --git a/pkg/api/frontendlogging/sentry.go b/pkg/api/frontendlogging/sentry.go index 032bb874ef1..66b74c09c04 100644 --- a/pkg/api/frontendlogging/sentry.go +++ b/pkg/api/frontendlogging/sentry.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/getsentry/sentry-go" + "github.com/grafana/grafana/pkg/infra/log" ) diff --git a/pkg/api/frontendlogging/source_maps.go b/pkg/api/frontendlogging/source_maps.go index 09536c24e5b..95a4802a2ab 100644 --- a/pkg/api/frontendlogging/source_maps.go +++ b/pkg/api/frontendlogging/source_maps.go @@ -9,9 +9,9 @@ import ( "strings" "sync" + "github.com/getsentry/sentry-go" sourcemap "github.com/go-sourcemap/sourcemap" - "github.com/getsentry/sentry-go" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/setting" ) diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 3c65148c382..ea05fdfdbaa 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -2,12 +2,13 @@ package api import ( "context" + "fmt" "net/http" "strconv" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/licensing" @@ -18,7 +19,7 @@ import ( "github.com/grafana/grafana/pkg/util" ) -func (hs *HTTPServer) GetFrontendSettings(c *models.ReqContext) { +func (hs *HTTPServer) GetFrontendSettings(c *contextmodel.ReqContext) { settings, err := hs.getFrontendSettingsMap(c) if err != nil { c.JsonApiErr(400, "Failed to get frontend settings", err) @@ -29,7 +30,7 @@ func (hs *HTTPServer) GetFrontendSettings(c *models.ReqContext) { } // getFrontendSettingsMap returns a json object with all the settings needed for front end initialisation. -func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]interface{}, error) { +func (hs *HTTPServer) getFrontendSettingsMap(c *contextmodel.ReqContext) (map[string]interface{}, error) { enabledPlugins, err := hs.enabledPlugins(c.Req.Context(), c.OrgID) if err != nil { return nil, err @@ -154,6 +155,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i "GrafanaComSkipOrgRoleSync": hs.Cfg.GrafanaComSkipOrgRoleSync, "GitLabSkipOrgRoleSync": hs.Cfg.GitLabSkipOrgRoleSync, "AzureADSkipOrgRoleSync": hs.Cfg.AzureADSkipOrgRoleSync, + "OktaSkipOrgRoleSync": hs.Cfg.OktaSkipOrgRoleSync, "DisableSyncLock": hs.Cfg.DisableSyncLock, }, "buildInfo": map[string]interface{}{ @@ -212,6 +214,14 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i "snapshotEnabled": hs.Cfg.SnapshotEnabled, } + if hs.pluginsCDNService != nil && hs.pluginsCDNService.IsEnabled() { + cdnBaseURL, err := hs.pluginsCDNService.BaseURL() + if err != nil { + return nil, fmt.Errorf("plugins cdn base url: %w", err) + } + jsonObj["pluginsCDNBaseURL"] = cdnBaseURL + } + if hs.ThumbService != nil { jsonObj["dashboardPreviews"] = hs.ThumbService.GetDashboardPreviewsSetupSettings(c) } @@ -231,7 +241,7 @@ func isSupportBundlesEnabled(hs *HTTPServer) bool { hs.Features.IsEnabled(featuremgmt.FlagSupportBundles) } -func (hs *HTTPServer) getFSDataSources(c *models.ReqContext, enabledPlugins EnabledPlugins) (map[string]plugins.DataSourceDTO, error) { +func (hs *HTTPServer) getFSDataSources(c *contextmodel.ReqContext, enabledPlugins EnabledPlugins) (map[string]plugins.DataSourceDTO, error) { orgDataSources := make([]*datasources.DataSource, 0) if c.OrgID != 0 { query := datasources.GetDataSourcesQuery{OrgId: c.OrgID, DataSourceLimit: hs.Cfg.DataSourceLimit} diff --git a/pkg/api/frontendsettings_test.go b/pkg/api/frontendsettings_test.go index dc1d1446a43..07d3a4d1ab5 100644 --- a/pkg/api/frontendsettings_test.go +++ b/pkg/api/frontendsettings_test.go @@ -7,12 +7,14 @@ import ( "path/filepath" "testing" - "github.com/grafana/grafana/pkg/login/social" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/login/social" "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/config" + "github.com/grafana/grafana/pkg/plugins/pluginscdn" accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/licensing" @@ -58,7 +60,11 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features *featuremgmt. grafanaUpdateChecker: &updatechecker.GrafanaService{}, AccessControl: accesscontrolmock.New().WithDisabled(), PluginSettings: pluginSettings.ProvideService(sqlStore, secretsService), - SocialService: social.ProvideService(cfg, features), + pluginsCDNService: pluginscdn.ProvideService(&config.Cfg{ + PluginsCDNURLTemplate: cfg.PluginsCDNURLTemplate, + PluginSettings: cfg.PluginSettings, + }), + SocialService: social.ProvideService(cfg, features), } m := web.New() @@ -138,3 +144,53 @@ func TestHTTPServer_GetFrontendSettings_hideVersionAnonymous(t *testing.T) { }) } } + +func TestHTTPServer_GetFrontendSettings_pluginsCDNBaseURL(t *testing.T) { + type settings struct { + PluginsCDNBaseURL string `json:"pluginsCDNBaseURL"` + } + + tests := []struct { + desc string + mutateCfg func(*setting.Cfg) + expected settings + }{ + { + desc: "With CDN", + mutateCfg: func(cfg *setting.Cfg) { + cfg.PluginsCDNURLTemplate = "https://cdn.example.com/{id}/{version}/public/plugins/{id}/{assetPath}" + }, + expected: settings{PluginsCDNBaseURL: "https://cdn.example.com"}, + }, + { + desc: "Without CDN", + mutateCfg: func(cfg *setting.Cfg) { + cfg.PluginsCDNURLTemplate = "" + }, + expected: settings{PluginsCDNBaseURL: ""}, + }, + { + desc: "CDN is disabled by default", + expected: settings{PluginsCDNBaseURL: ""}, + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + cfg := setting.NewCfg() + if test.mutateCfg != nil { + test.mutateCfg(cfg) + } + m, _ := setupTestEnvironment(t, cfg, featuremgmt.WithFeatures()) + req := httptest.NewRequest(http.MethodGet, "/api/frontend/settings", nil) + + recorder := httptest.NewRecorder() + m.ServeHTTP(recorder, req) + var got settings + err := json.Unmarshal(recorder.Body.Bytes(), &got) + require.NoError(t, err) + require.Equal(t, http.StatusOK, recorder.Code) + require.EqualValues(t, test.expected, got) + }) + } +} diff --git a/pkg/api/grafana_com_proxy.go b/pkg/api/grafana_com_proxy.go index ca9cf6a71bb..088b745b82f 100644 --- a/pkg/api/grafana_com_proxy.go +++ b/pkg/api/grafana_com_proxy.go @@ -8,7 +8,7 @@ import ( "time" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util/proxyutil" "github.com/grafana/grafana/pkg/web" @@ -23,15 +23,15 @@ var grafanaComProxyTransport = &http.Transport{ TLSHandshakeTimeout: 10 * time.Second, } -func ReverseProxyGnetReq(logger log.Logger, proxyPath string, version string, grafanaComUrl string) *httputil.ReverseProxy { - url, _ := url.Parse(grafanaComUrl) +func ReverseProxyGnetReq(logger log.Logger, proxyPath string, version string, grafanaComAPIUrl string) *httputil.ReverseProxy { + url, _ := url.Parse(grafanaComAPIUrl) director := func(req *http.Request) { req.URL.Scheme = url.Scheme req.URL.Host = url.Host req.Host = url.Host - req.URL.Path = util.JoinURLFragments(url.Path+"/api", proxyPath) + req.URL.Path = util.JoinURLFragments(url.Path, proxyPath) // clear cookie headers req.Header.Del("Cookie") @@ -45,7 +45,7 @@ func ReverseProxyGnetReq(logger log.Logger, proxyPath string, version string, gr return proxyutil.NewReverseProxy(logger, director) } -func (hs *HTTPServer) ProxyGnetRequest(c *models.ReqContext) { +func (hs *HTTPServer) ProxyGnetRequest(c *contextmodel.ReqContext) { proxyPath := web.Params(c.Req)["*"] proxy := ReverseProxyGnetReq(c.Logger, proxyPath, hs.Cfg.BuildVersion, hs.Cfg.GrafanaComAPIURL) proxy.Transport = grafanaComProxyTransport diff --git a/pkg/api/health_test.go b/pkg/api/health_test.go index 699f85607af..96ab7e2612e 100644 --- a/pkg/api/health_test.go +++ b/pkg/api/health_test.go @@ -7,11 +7,12 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/db/dbtest" "github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web" - "github.com/stretchr/testify/require" ) func TestHealthAPI_Version(t *testing.T) { diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index c3500501a20..b6348daf98e 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -33,6 +33,7 @@ import ( "github.com/grafana/grafana/pkg/middleware/csrf" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/plugincontext" + "github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/registry/corekind" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/alerting" @@ -201,6 +202,7 @@ type HTTPServer struct { playlistService playlist.Service apiKeyService apikey.Service kvStore kvstore.KVStore + pluginsCDNService *pluginscdn.Service userService user.Service tempUserService tempUser.Service @@ -258,6 +260,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi annotationRepo annotations.Repository, tagService tag.Service, searchv2HTTPService searchV2.SearchHTTPService, queryLibraryHTTPService querylibrary.HTTPService, queryLibraryService querylibrary.Service, oauthTokenService oauthtoken.OAuthTokenService, statsService stats.Service, authnService authn.Service, + pluginsCDNService *pluginscdn.Service, k8saccess k8saccess.K8SAccess, // required so that the router is registered starApi *starApi.API, ) (*HTTPServer, error) { @@ -366,6 +369,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi oauthTokenService: oauthTokenService, statsService: statsService, authnService: authnService, + pluginsCDNService: pluginsCDNService, starApi: starApi, } if hs.Listener != nil { diff --git a/pkg/api/http_server_test.go b/pkg/api/http_server_test.go index c0d45258855..5cb99004bef 100644 --- a/pkg/api/http_server_test.go +++ b/pkg/api/http_server_test.go @@ -3,8 +3,9 @@ package api import ( "testing" - "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/assert" + + "github.com/grafana/grafana/pkg/setting" ) func TestHTTPServer_MetricsBasicAuth(t *testing.T) { diff --git a/pkg/api/index.go b/pkg/api/index.go index 7037121e86b..2aee274b792 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -6,8 +6,8 @@ import ( "strings" "github.com/grafana/grafana/pkg/api/dtos" - "github.com/grafana/grafana/pkg/models" ac "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" @@ -17,11 +17,12 @@ import ( const ( // Themes - lightName = "light" - darkName = "dark" + lightName = "light" + darkName = "dark" + systemName = "system" ) -func (hs *HTTPServer) editorInAnyFolder(c *models.ReqContext) bool { +func (hs *HTTPServer) editorInAnyFolder(c *contextmodel.ReqContext) bool { hasEditPermissionInFoldersQuery := folder.HasEditPermissionInFoldersQuery{SignedInUser: c.SignedInUser} hasEditPermissionInFoldersQueryResult, err := hs.DashboardService.HasEditPermissionInFolders(c.Req.Context(), &hasEditPermissionInFoldersQuery) if err != nil { @@ -30,7 +31,7 @@ func (hs *HTTPServer) editorInAnyFolder(c *models.ReqContext) bool { return hasEditPermissionInFoldersQueryResult } -func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewData, error) { +func (hs *HTTPServer) setIndexViewData(c *contextmodel.ReqContext) (*dtos.IndexViewData, error) { hasAccess := ac.HasAccess(hs.AccessControl, c) hasEditPerm := hasAccess(hs.editorInAnyFolder, ac.EvalAny(ac.EvalPermission(dashboards.ActionDashboardsCreate), ac.EvalPermission(dashboards.ActionFoldersCreate))) @@ -100,6 +101,7 @@ func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewDat OrgRole: c.OrgRole, GravatarUrl: dtos.GetGravatarUrl(c.Email), IsGrafanaAdmin: c.IsGrafanaAdmin, + Theme: prefs.Theme, LightTheme: prefs.Theme == lightName, Timezone: prefs.Timezone, WeekStart: weekStart, @@ -150,12 +152,9 @@ func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewDat } themeURLParam := c.Query("theme") - if themeURLParam == lightName { - data.User.LightTheme = true - data.Theme = lightName - } else if themeURLParam == darkName { - data.User.LightTheme = false - data.Theme = darkName + if themeURLParam == lightName || themeURLParam == darkName || themeURLParam == systemName { + data.User.Theme = themeURLParam + data.Theme = themeURLParam } hs.HooksService.RunIndexDataHooks(&data, c) @@ -167,7 +166,7 @@ func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewDat return &data, nil } -func (hs *HTTPServer) Index(c *models.ReqContext) { +func (hs *HTTPServer) Index(c *contextmodel.ReqContext) { data, err := hs.setIndexViewData(c) if err != nil { c.Handle(hs.Cfg, 500, "Failed to get settings", err) @@ -176,7 +175,7 @@ func (hs *HTTPServer) Index(c *models.ReqContext) { c.HTML(http.StatusOK, "index", data) } -func (hs *HTTPServer) NotFoundHandler(c *models.ReqContext) { +func (hs *HTTPServer) NotFoundHandler(c *contextmodel.ReqContext) { if c.IsApiRequest() { c.JsonApiErr(404, "Not found", nil) return diff --git a/pkg/api/ldap_debug.go b/pkg/api/ldap_debug.go index 875e838f8f8..a6907ad7b93 100644 --- a/pkg/api/ldap_debug.go +++ b/pkg/api/ldap_debug.go @@ -10,7 +10,7 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/ldap" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/multildap" @@ -47,14 +47,14 @@ type LDAPRoleDTO struct { // LDAPUserDTO is a serializer for users mapped from LDAP type LDAPUserDTO struct { - Name *LDAPAttribute `json:"name"` - Surname *LDAPAttribute `json:"surname"` - Email *LDAPAttribute `json:"email"` - Username *LDAPAttribute `json:"login"` - IsGrafanaAdmin *bool `json:"isGrafanaAdmin"` - IsDisabled bool `json:"isDisabled"` - OrgRoles []LDAPRoleDTO `json:"roles"` - Teams []models.TeamOrgGroupDTO `json:"teams"` + Name *LDAPAttribute `json:"name"` + Surname *LDAPAttribute `json:"surname"` + Email *LDAPAttribute `json:"email"` + Username *LDAPAttribute `json:"login"` + IsGrafanaAdmin *bool `json:"isGrafanaAdmin"` + IsDisabled bool `json:"isDisabled"` + OrgRoles []LDAPRoleDTO `json:"roles"` + Teams []ldap.TeamOrgGroupDTO `json:"teams"` } // LDAPServerDTO is a serializer for LDAP server statuses @@ -117,7 +117,7 @@ func (user *LDAPUserDTO) FetchOrgs(ctx context.Context, orga org.Service) error // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) ReloadLDAPCfg(c *models.ReqContext) response.Response { +func (hs *HTTPServer) ReloadLDAPCfg(c *contextmodel.ReqContext) response.Response { if !ldap.IsEnabled() { return response.Error(http.StatusBadRequest, "LDAP is not enabled", nil) } @@ -143,7 +143,7 @@ func (hs *HTTPServer) ReloadLDAPCfg(c *models.ReqContext) response.Response { // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) GetLDAPStatus(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetLDAPStatus(c *contextmodel.ReqContext) response.Response { if !ldap.IsEnabled() { return response.Error(http.StatusBadRequest, "LDAP is not enabled", nil) } @@ -196,7 +196,7 @@ func (hs *HTTPServer) GetLDAPStatus(c *models.ReqContext) response.Response { // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) PostSyncUserWithLDAP(c *models.ReqContext) response.Response { +func (hs *HTTPServer) PostSyncUserWithLDAP(c *contextmodel.ReqContext) response.Response { if !ldap.IsEnabled() { return response.Error(http.StatusBadRequest, "LDAP is not enabled", nil) } @@ -222,7 +222,7 @@ func (hs *HTTPServer) PostSyncUserWithLDAP(c *models.ReqContext) response.Respon return response.Error(500, "Failed to get user", err) } - authModuleQuery := &models.GetAuthInfoQuery{UserId: usr.ID, AuthModule: login.LDAPAuthModule} + authModuleQuery := &login.GetAuthInfoQuery{UserId: usr.ID, AuthModule: login.LDAPAuthModule} if err := hs.authInfoService.GetAuthInfo(c.Req.Context(), authModuleQuery); err != nil { // validate the userId comes from LDAP if errors.Is(err, user.ErrUserNotFound) { return response.Error(404, user.ErrUserNotFound.Error(), nil) @@ -259,11 +259,11 @@ func (hs *HTTPServer) PostSyncUserWithLDAP(c *models.ReqContext) response.Respon return response.Error(http.StatusBadRequest, "Something went wrong while finding the user in LDAP", err) } - upsertCmd := &models.UpsertUserCommand{ + upsertCmd := &login.UpsertUserCommand{ ReqContext: c, ExternalUser: userInfo, SignupAllowed: hs.Cfg.LDAPAllowSignup, - UserLookupParams: models.UserLookupParams{ + UserLookupParams: login.UserLookupParams{ UserID: &usr.ID, // Upsert by ID only Email: nil, Login: nil, @@ -292,7 +292,7 @@ func (hs *HTTPServer) PostSyncUserWithLDAP(c *models.ReqContext) response.Respon // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) GetUserFromLDAP(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetUserFromLDAP(c *contextmodel.ReqContext) response.Response { if !ldap.IsEnabled() { return response.Error(http.StatusBadRequest, "LDAP is not enabled", nil) } diff --git a/pkg/api/ldap_debug_test.go b/pkg/api/ldap_debug_test.go index e95a4bbeb5c..5e4e51fd750 100644 --- a/pkg/api/ldap_debug_test.go +++ b/pkg/api/ldap_debug_test.go @@ -14,10 +14,11 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/infra/db/dbtest" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/auth/authtest" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/ldap" + "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/login/loginservice" "github.com/grafana/grafana/pkg/services/login/logintest" "github.com/grafana/grafana/pkg/services/multildap" @@ -30,10 +31,10 @@ import ( ) type LDAPMock struct { - Results []*models.ExternalUserInfo + Results []*login.ExternalUserInfo } -var userSearchResult *models.ExternalUserInfo +var userSearchResult *login.ExternalUserInfo var userSearchConfig ldap.ServerConfig var userSearchError error var pingResult []*multildap.ServerStatus @@ -43,16 +44,16 @@ func (m *LDAPMock) Ping() ([]*multildap.ServerStatus, error) { return pingResult, pingError } -func (m *LDAPMock) Login(query *models.LoginUserQuery) (*models.ExternalUserInfo, error) { - return &models.ExternalUserInfo{}, nil +func (m *LDAPMock) Login(query *login.LoginUserQuery) (*login.ExternalUserInfo, error) { + return &login.ExternalUserInfo{}, nil } -func (m *LDAPMock) Users(logins []string) ([]*models.ExternalUserInfo, error) { - s := []*models.ExternalUserInfo{} +func (m *LDAPMock) Users(logins []string) ([]*login.ExternalUserInfo, error) { + s := []*login.ExternalUserInfo{} return s, nil } -func (m *LDAPMock) User(login string) (*models.ExternalUserInfo, ldap.ServerConfig, error) { +func (m *LDAPMock) User(login string) (*login.ExternalUserInfo, ldap.ServerConfig, error) { return userSearchResult, userSearchConfig, userSearchError } @@ -71,7 +72,7 @@ func getUserFromLDAPContext(t *testing.T, requestURL string, searchOrgRst []*org hs := &HTTPServer{Cfg: setting.NewCfg(), ldapGroups: ldap.ProvideGroupsService(), orgService: &orgtest.FakeOrgService{ExpectedOrgs: searchOrgRst}} - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { sc.context = c return hs.GetUserFromLDAP(c) }) @@ -105,7 +106,7 @@ func TestGetUserFromLDAPAPIEndpoint_UserNotFound(t *testing.T) { func TestGetUserFromLDAPAPIEndpoint_OrgNotfound(t *testing.T) { isAdmin := true - userSearchResult = &models.ExternalUserInfo{ + userSearchResult = &login.ExternalUserInfo{ Name: "John Doe", Email: "john.doe@example.com", Login: "johndoe", @@ -160,7 +161,7 @@ func TestGetUserFromLDAPAPIEndpoint_OrgNotfound(t *testing.T) { func TestGetUserFromLDAPAPIEndpoint(t *testing.T) { isAdmin := true - userSearchResult = &models.ExternalUserInfo{ + userSearchResult = &login.ExternalUserInfo{ Name: "John Doe", Email: "john.doe@example.com", Login: "johndoe", @@ -235,7 +236,7 @@ func TestGetUserFromLDAPAPIEndpoint(t *testing.T) { func TestGetUserFromLDAPAPIEndpoint_WithTeamHandler(t *testing.T) { isAdmin := true - userSearchResult = &models.ExternalUserInfo{ + userSearchResult = &login.ExternalUserInfo{ Name: "John Doe", Email: "john.doe@example.com", Login: "johndoe", @@ -318,7 +319,7 @@ func getLDAPStatusContext(t *testing.T) *scenarioContext { hs := &HTTPServer{Cfg: setting.NewCfg()} - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { sc.context = c return hs.GetLDAPStatus(c) }) @@ -386,7 +387,7 @@ func postSyncUserWithLDAPContext(t *testing.T, requestURL string, preHook func(* userService: userService, } - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { sc.context = c return hs.PostSyncUserWithLDAP(c) }) @@ -417,7 +418,7 @@ func TestPostSyncUserWithLDAPAPIEndpoint_Success(t *testing.T) { return &LDAPMock{} } - userSearchResult = &models.ExternalUserInfo{ + userSearchResult = &login.ExternalUserInfo{ Login: "ldap-daniel", } }, userServiceMock) @@ -486,7 +487,7 @@ func TestPostSyncUserWithLDAPAPIEndpoint_WhenUserNotInLDAP(t *testing.T) { userServiceMock := usertest.NewUserServiceFake() userServiceMock.ExpectedUser = &user.User{Login: "ldap-daniel", ID: 34} sc := postSyncUserWithLDAPContext(t, "/api/admin/ldap/sync/34", func(t *testing.T, sc *scenarioContext) { - sc.authInfoService.ExpectedExternalUser = &models.ExternalUserInfo{IsDisabled: true, UserId: 34} + sc.authInfoService.ExpectedExternalUser = &login.ExternalUserInfo{IsDisabled: true, UserId: 34} getLDAPConfig = func(*setting.Cfg) (*ldap.Config, error) { return &ldap.Config{}, nil } @@ -624,7 +625,7 @@ func TestLDAP_AccessControl(t *testing.T) { hs.authInfoService = &logintest.AuthInfoServiceFake{} }) // Add minimal setup to pass handler - userSearchResult = &models.ExternalUserInfo{} + userSearchResult = &login.ExternalUserInfo{} userSearchError = nil newLDAP = func(_ []*ldap.ServerConfig) multildap.IMultiLDAP { return &LDAPMock{} diff --git a/pkg/api/login.go b/pkg/api/login.go index c2d81a77636..8d041941ff4 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -15,11 +15,11 @@ import ( "github.com/grafana/grafana/pkg/infra/network" "github.com/grafana/grafana/pkg/login" "github.com/grafana/grafana/pkg/middleware/cookies" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/authn" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/featuremgmt" - loginService "github.com/grafana/grafana/pkg/services/login" + loginservice "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" @@ -80,7 +80,7 @@ func (hs *HTTPServer) CookieOptionsFromCfg() cookies.CookieOptions { } } -func (hs *HTTPServer) LoginView(c *models.ReqContext) { +func (hs *HTTPServer) LoginView(c *contextmodel.ReqContext) { viewData, err := setIndexViewData(hs, c) if err != nil { c.Handle(hs.Cfg, 500, "Failed to get settings", err) @@ -139,7 +139,7 @@ func (hs *HTTPServer) LoginView(c *models.ReqContext) { c.HTML(http.StatusOK, getViewIndex(), viewData) } -func (hs *HTTPServer) tryAutoLogin(c *models.ReqContext) bool { +func (hs *HTTPServer) tryAutoLogin(c *contextmodel.ReqContext) bool { samlAutoLogin := hs.samlAutoLoginEnabled() oauthInfos := hs.SocialService.GetOAuthInfoProviders() @@ -185,7 +185,7 @@ func (hs *HTTPServer) tryAutoLogin(c *models.ReqContext) bool { return false } -func (hs *HTTPServer) LoginAPIPing(c *models.ReqContext) response.Response { +func (hs *HTTPServer) LoginAPIPing(c *contextmodel.ReqContext) response.Response { if c.IsSignedIn || c.IsAnonymous { return response.JSON(http.StatusOK, "Logged in") } @@ -193,7 +193,7 @@ func (hs *HTTPServer) LoginAPIPing(c *models.ReqContext) response.Response { return response.Error(401, "Unauthorized", nil) } -func (hs *HTTPServer) LoginPost(c *models.ReqContext) response.Response { +func (hs *HTTPServer) LoginPost(c *contextmodel.ReqContext) response.Response { if hs.Features.IsEnabled(featuremgmt.FlagAuthnService) { identity, err := hs.authnService.Login(c.Req.Context(), authn.ClientForm, &authn.Request{HTTPRequest: c.Req, Resp: c.Resp}) if err != nil { @@ -235,7 +235,7 @@ func (hs *HTTPServer) LoginPost(c *models.ReqContext) response.Response { if err == nil && resp.ErrMessage() != "" { err = errors.New(resp.ErrMessage()) } - hs.HooksService.RunLoginHook(&models.LoginInfo{ + hs.HooksService.RunLoginHook(&loginservice.LoginInfo{ AuthModule: authModule, User: usr, LoginUsername: cmd.User, @@ -249,7 +249,7 @@ func (hs *HTTPServer) LoginPost(c *models.ReqContext) response.Response { return resp } - authQuery := &models.LoginUserQuery{ + authQuery := &loginservice.LoginUserQuery{ ReqContext: c, Username: cmd.User, Password: cmd.Password, @@ -313,7 +313,7 @@ func (hs *HTTPServer) LoginPost(c *models.ReqContext) response.Response { return resp } -func (hs *HTTPServer) loginUserWithUser(user *user.User, c *models.ReqContext) error { +func (hs *HTTPServer) loginUserWithUser(user *user.User, c *contextmodel.ReqContext) error { if user == nil { return errors.New("could not login user") } @@ -326,7 +326,7 @@ func (hs *HTTPServer) loginUserWithUser(user *user.User, c *models.ReqContext) e } hs.log.Debug("Got IP address from client address", "addr", addr, "ip", ip) - ctx := context.WithValue(c.Req.Context(), models.RequestURIKey{}, c.Req.RequestURI) + ctx := context.WithValue(c.Req.Context(), loginservice.RequestURIKey{}, c.Req.RequestURI) userToken, err := hs.AuthTokenService.CreateToken(ctx, user, ip, c.Req.UserAgent()) if err != nil { return fmt.Errorf("%v: %w", "failed to create auth token", err) @@ -338,12 +338,12 @@ func (hs *HTTPServer) loginUserWithUser(user *user.User, c *models.ReqContext) e return nil } -func (hs *HTTPServer) Logout(c *models.ReqContext) { +func (hs *HTTPServer) Logout(c *contextmodel.ReqContext) { // If SAML is enabled and this is a SAML user use saml logout if hs.samlSingleLogoutEnabled() { - getAuthQuery := models.GetAuthInfoQuery{UserId: c.UserID} + getAuthQuery := loginservice.GetAuthInfoQuery{UserId: c.UserID} if err := hs.authInfoService.GetAuthInfo(c.Req.Context(), &getAuthQuery); err == nil { - if getAuthQuery.Result.AuthModule == loginService.SAMLAuthModule { + if getAuthQuery.Result.AuthModule == loginservice.SAMLAuthModule { c.Redirect(hs.Cfg.AppSubURL + "/logout/saml") return } @@ -372,7 +372,7 @@ func (hs *HTTPServer) Logout(c *models.ReqContext) { } } -func (hs *HTTPServer) tryGetEncryptedCookie(ctx *models.ReqContext, cookieName string) (string, bool) { +func (hs *HTTPServer) tryGetEncryptedCookie(ctx *contextmodel.ReqContext, cookieName string) (string, bool) { cookie := ctx.GetCookie(cookieName) if cookie == "" { return "", false @@ -387,7 +387,7 @@ func (hs *HTTPServer) tryGetEncryptedCookie(ctx *models.ReqContext, cookieName s return string(decryptedError), err == nil } -func (hs *HTTPServer) trySetEncryptedCookie(ctx *models.ReqContext, cookieName string, value string, maxAge int) error { +func (hs *HTTPServer) trySetEncryptedCookie(ctx *contextmodel.ReqContext, cookieName string, value string, maxAge int) error { encryptedError, err := hs.SecretsService.Encrypt(ctx.Req.Context(), []byte(value), secrets.WithoutScope()) if err != nil { return err @@ -398,7 +398,7 @@ func (hs *HTTPServer) trySetEncryptedCookie(ctx *models.ReqContext, cookieName s return nil } -func (hs *HTTPServer) redirectWithError(ctx *models.ReqContext, err error, v ...interface{}) { +func (hs *HTTPServer) redirectWithError(ctx *contextmodel.ReqContext, err error, v ...interface{}) { ctx.Logger.Warn(err.Error(), v...) if err := hs.trySetEncryptedCookie(ctx, loginErrorCookieName, getLoginExternalError(err), 60); err != nil { hs.log.Error("Failed to set encrypted cookie", "err", err) @@ -407,7 +407,7 @@ func (hs *HTTPServer) redirectWithError(ctx *models.ReqContext, err error, v ... ctx.Redirect(hs.Cfg.AppSubURL + "/login") } -func (hs *HTTPServer) RedirectResponseWithError(ctx *models.ReqContext, err error, v ...interface{}) *response.RedirectResponse { +func (hs *HTTPServer) RedirectResponseWithError(ctx *contextmodel.ReqContext, err error, v ...interface{}) *response.RedirectResponse { ctx.Logger.Error(err.Error(), v...) if err := hs.trySetEncryptedCookie(ctx, loginErrorCookieName, getLoginExternalError(err), 60); err != nil { hs.log.Error("Failed to set encrypted cookie", "err", err) diff --git a/pkg/api/login_oauth.go b/pkg/api/login_oauth.go index 84bb08d7fe4..9572f2f0f59 100644 --- a/pkg/api/login_oauth.go +++ b/pkg/api/login_oauth.go @@ -17,10 +17,14 @@ import ( "github.com/grafana/grafana/pkg/login" "github.com/grafana/grafana/pkg/login/social" "github.com/grafana/grafana/pkg/middleware/cookies" - "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/authn" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" + "github.com/grafana/grafana/pkg/services/featuremgmt" + loginservice "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util/errutil" "github.com/grafana/grafana/pkg/web" ) @@ -68,12 +72,60 @@ func genPKCECode() (string, string, error) { return string(ascii), pkce, nil } -func (hs *HTTPServer) OAuthLogin(ctx *models.ReqContext) { - loginInfo := models.LoginInfo{ - AuthModule: "oauth", - } +func (hs *HTTPServer) OAuthLogin(ctx *contextmodel.ReqContext) { name := web.Params(ctx.Req)[":name"] - loginInfo.AuthModule = name + loginInfo := loginservice.LoginInfo{AuthModule: name} + + if errorParam := ctx.Query("error"); errorParam != "" { + errorDesc := ctx.Query("error_description") + oauthLogger.Error("failed to login ", "error", errorParam, "errorDesc", errorDesc) + hs.handleOAuthLoginErrorWithRedirect(ctx, loginInfo, login.ErrProviderDeniedRequest, "error", errorParam, "errorDesc", errorDesc) + return + } + + code := ctx.Query("code") + + if hs.Features.IsEnabled(featuremgmt.FlagAuthnService) { + req := &authn.Request{HTTPRequest: ctx.Req, Resp: ctx.Resp} + if code == "" { + redirect, err := hs.authnService.RedirectURL(ctx.Req.Context(), authn.ClientWithPrefix(name), req) + if err != nil { + hs.handleAuthnOAuthErr(ctx, "failed to generate oauth redirect url", err) + return + } + + if pkce := redirect.Extra[authn.KeyOAuthPKCE]; pkce != "" { + cookies.WriteCookie(ctx.Resp, OauthPKCECookieName, pkce, hs.Cfg.OAuthCookieMaxAge, hs.CookieOptionsFromCfg) + } + + cookies.WriteCookie(ctx.Resp, OauthStateCookieName, redirect.Extra[authn.KeyOAuthState], hs.Cfg.OAuthCookieMaxAge, hs.CookieOptionsFromCfg) + ctx.Redirect(redirect.URL) + return + } + + identity, err := hs.authnService.Login(ctx.Req.Context(), authn.ClientWithPrefix(name), req) + // NOTE: always delete these cookies, even if login failed + cookies.DeleteCookie(ctx.Resp, OauthPKCECookieName, hs.CookieOptionsFromCfg) + cookies.DeleteCookie(ctx.Resp, OauthStateCookieName, hs.CookieOptionsFromCfg) + + if err != nil { + hs.handleAuthnOAuthErr(ctx, "failed to perform login for oauth request", err) + return + } + + metrics.MApiLoginOAuth.Inc() + cookies.WriteSessionCookie(ctx, hs.Cfg, identity.SessionToken.UnhashedToken, hs.Cfg.LoginMaxLifetime) + + redirectURL := setting.AppSubUrl + "/" + if redirectTo := ctx.GetCookie("redirect_to"); len(redirectTo) > 0 && hs.ValidateRedirectTo(redirectTo) == nil { + redirectURL = redirectTo + cookies.DeleteCookie(ctx.Resp, "redirect_to", hs.CookieOptionsFromCfg) + } + + ctx.Redirect(redirectURL) + return + } + provider := hs.SocialService.GetOAuthInfoProvider(name) if provider == nil { hs.handleOAuthLoginErrorWithRedirect(ctx, loginInfo, errors.New("OAuth not enabled")) @@ -86,15 +138,6 @@ func (hs *HTTPServer) OAuthLogin(ctx *models.ReqContext) { return } - errorParam := ctx.Query("error") - if errorParam != "" { - errorDesc := ctx.Query("error_description") - oauthLogger.Error("failed to login ", "error", errorParam, "errorDesc", errorDesc) - hs.handleOAuthLoginErrorWithRedirect(ctx, loginInfo, login.ErrProviderDeniedRequest, "error", errorParam, "errorDesc", errorDesc) - return - } - - code := ctx.Query("code") if code == "" { var opts []oauth2.AuthCodeOption if provider.UsePKCE { @@ -105,6 +148,7 @@ func (hs *HTTPServer) OAuthLogin(ctx *models.ReqContext) { HttpStatus: http.StatusInternalServerError, PublicMessage: "An internal error occurred", }) + return } cookies.WriteCookie(ctx.Resp, OauthPKCECookieName, ascii, hs.Cfg.OAuthCookieMaxAge, hs.CookieOptionsFromCfg) @@ -270,10 +314,10 @@ func (hs *HTTPServer) OAuthLogin(ctx *models.ReqContext) { } // buildExternalUserInfo returns a ExternalUserInfo struct from OAuth user profile -func (hs *HTTPServer) buildExternalUserInfo(token *oauth2.Token, userInfo *social.BasicUserInfo, name string) *models.ExternalUserInfo { +func (hs *HTTPServer) buildExternalUserInfo(token *oauth2.Token, userInfo *social.BasicUserInfo, name string) *loginservice.ExternalUserInfo { oauthLogger.Debug("Building external user info from OAuth user info") - extUser := &models.ExternalUserInfo{ + extUser := &loginservice.ExternalUserInfo{ AuthModule: fmt.Sprintf("oauth_%s", name), OAuthToken: token, AuthId: userInfo.Id, @@ -308,17 +352,17 @@ func (hs *HTTPServer) buildExternalUserInfo(token *oauth2.Token, userInfo *socia // SyncUser syncs a Grafana user profile with the corresponding OAuth profile. func (hs *HTTPServer) SyncUser( - ctx *models.ReqContext, - extUser *models.ExternalUserInfo, + ctx *contextmodel.ReqContext, + extUser *loginservice.ExternalUserInfo, connect social.SocialConnector, ) (*user.User, error) { oauthLogger.Debug("Syncing Grafana user with corresponding OAuth profile") // add/update user in Grafana - cmd := &models.UpsertUserCommand{ + cmd := &loginservice.UpsertUserCommand{ ReqContext: ctx, ExternalUser: extUser, SignupAllowed: connect.IsSignupAllowed(), - UserLookupParams: models.UserLookupParams{ + UserLookupParams: loginservice.UserLookupParams{ Email: &extUser.Email, UserID: nil, Login: nil, @@ -344,27 +388,46 @@ func (hs *HTTPServer) hashStatecode(code, seed string) string { return hex.EncodeToString(hashBytes[:]) } +func (hs *HTTPServer) handleAuthnOAuthErr(c *contextmodel.ReqContext, msg string, err error) { + gfErr := &errutil.Error{} + if errors.As(err, gfErr) { + if gfErr.Public().Message != "" { + c.Handle(hs.Cfg, gfErr.Public().StatusCode, gfErr.Public().Message, err) + return + } + } + + c.Logger.Warn(msg, "err", err) + c.Redirect(hs.Cfg.AppSubURL + "/login") +} + type LoginError struct { HttpStatus int PublicMessage string Err error } -func (hs *HTTPServer) handleOAuthLoginError(ctx *models.ReqContext, info models.LoginInfo, err LoginError) { +func (hs *HTTPServer) handleOAuthLoginError(ctx *contextmodel.ReqContext, info loginservice.LoginInfo, err LoginError) { ctx.Handle(hs.Cfg, err.HttpStatus, err.PublicMessage, err.Err) - info.Error = err.Err - if info.Error == nil { - info.Error = errors.New(err.PublicMessage) - } - info.HTTPStatus = err.HttpStatus + // login hooks is handled by authn.Service + if !hs.Features.IsEnabled(featuremgmt.FlagAuthnService) { + info.Error = err.Err + if info.Error == nil { + info.Error = errors.New(err.PublicMessage) + } + info.HTTPStatus = err.HttpStatus - hs.HooksService.RunLoginHook(&info, ctx) + hs.HooksService.RunLoginHook(&info, ctx) + } } -func (hs *HTTPServer) handleOAuthLoginErrorWithRedirect(ctx *models.ReqContext, info models.LoginInfo, err error, v ...interface{}) { +func (hs *HTTPServer) handleOAuthLoginErrorWithRedirect(ctx *contextmodel.ReqContext, info loginservice.LoginInfo, err error, v ...interface{}) { hs.redirectWithError(ctx, err, v...) - info.Error = err - hs.HooksService.RunLoginHook(&info, ctx) + // login hooks is handled by authn.Service + if !hs.Features.IsEnabled(featuremgmt.FlagAuthnService) { + info.Error = err + hs.HooksService.RunLoginHook(&info, ctx) + } } diff --git a/pkg/api/login_oauth_test.go b/pkg/api/login_oauth_test.go index b8b798e1deb..cbd1614a9dc 100644 --- a/pkg/api/login_oauth_test.go +++ b/pkg/api/login_oauth_test.go @@ -34,6 +34,7 @@ func setupSocialHTTPServerWithConfig(t *testing.T, cfg *setting.Cfg) *HTTPServer SocialService: social.ProvideService(cfg, featuremgmt.WithFeatures()), HooksService: hooks.ProvideService(), SecretsService: fakes.NewFakeSecretsService(), + Features: featuremgmt.WithFeatures(), } } diff --git a/pkg/api/login_test.go b/pkg/api/login_test.go index 9e09ba3deaa..402d9d77b7e 100644 --- a/pkg/api/login_test.go +++ b/pkg/api/login_test.go @@ -12,6 +12,9 @@ import ( "strings" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" @@ -19,8 +22,8 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/login" "github.com/grafana/grafana/pkg/login/social" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/auth/authtest" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/hooks" "github.com/grafana/grafana/pkg/services/licensing" @@ -31,8 +34,6 @@ import ( secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func fakeSetIndexViewData(t *testing.T) { @@ -40,7 +41,7 @@ func fakeSetIndexViewData(t *testing.T) { t.Cleanup(func() { setIndexViewData = origSetIndexViewData }) - setIndexViewData = func(*HTTPServer, *models.ReqContext) (*dtos.IndexViewData, error) { + setIndexViewData = func(*HTTPServer, *contextmodel.ReqContext) (*dtos.IndexViewData, error) { data := &dtos.IndexViewData{ User: &dtos.CurrentUser{}, Settings: map[string]interface{}{}, @@ -104,7 +105,7 @@ func TestLoginErrorCookieAPIEndpoint(t *testing.T) { SecretsService: secretsService, } - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { hs.LoginView(c) return response.Empty(http.StatusOK) }) @@ -152,7 +153,7 @@ func TestLoginViewRedirect(t *testing.T) { } hs.Cfg.CookieSecure = true - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.IsSignedIn = true c.SignedInUser = &user.SignedInUser{ UserID: 10, @@ -328,7 +329,7 @@ func TestLoginPostRedirect(t *testing.T) { } hs.Cfg.CookieSecure = true - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Header.Set("Content-Type", "application/json") c.Req.Body = io.NopCloser(bytes.NewBufferString(`{"user":"admin","password":"admin"}`)) return hs.LoginPost(c) @@ -492,7 +493,7 @@ func TestLoginOAuthRedirect(t *testing.T) { SocialService: mock, } - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { hs.LoginView(c) return response.Empty(http.StatusOK) }) @@ -518,7 +519,7 @@ func TestLoginInternal(t *testing.T) { log: log.New("test"), } - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.URL.RawQuery = "disableAutoLogin=true" hs.LoginView(c) return response.Empty(http.StatusOK) @@ -570,7 +571,7 @@ func setupAuthProxyLoginTest(t *testing.T, enableLoginToken bool) *scenarioConte SocialService: &mockSocialService{}, } - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.IsSignedIn = true c.SignedInUser = &user.SignedInUser{ UserID: 10, @@ -589,10 +590,10 @@ func setupAuthProxyLoginTest(t *testing.T, enableLoginToken bool) *scenarioConte } type loginHookTest struct { - info *models.LoginInfo + info *loginservice.LoginInfo } -func (r *loginHookTest) LoginHook(loginInfo *models.LoginInfo, req *models.ReqContext) { +func (r *loginHookTest) LoginHook(loginInfo *loginservice.LoginInfo, req *contextmodel.ReqContext) { r.info = loginInfo } @@ -608,7 +609,7 @@ func TestLoginPostRunLokingHook(t *testing.T) { HooksService: hookService, } - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Header.Set("Content-Type", "application/json") c.Req.Body = io.NopCloser(bytes.NewBufferString(`{"user":"admin","password":"admin"}`)) x := hs.LoginPost(c) @@ -628,12 +629,12 @@ func TestLoginPostRunLokingHook(t *testing.T) { authUser *user.User authModule string authErr error - info models.LoginInfo + info loginservice.LoginInfo }{ { desc: "invalid credentials", authErr: login.ErrInvalidCredentials, - info: models.LoginInfo{ + info: loginservice.LoginInfo{ AuthModule: "", HTTPStatus: 401, Error: login.ErrInvalidCredentials, @@ -642,7 +643,7 @@ func TestLoginPostRunLokingHook(t *testing.T) { { desc: "user disabled", authErr: login.ErrUserDisabled, - info: models.LoginInfo{ + info: loginservice.LoginInfo{ AuthModule: "", HTTPStatus: 401, Error: login.ErrUserDisabled, @@ -652,7 +653,7 @@ func TestLoginPostRunLokingHook(t *testing.T) { desc: "valid Grafana user", authUser: testUser, authModule: "grafana", - info: models.LoginInfo{ + info: loginservice.LoginInfo{ AuthModule: "grafana", User: testUser, HTTPStatus: 200, @@ -662,7 +663,7 @@ func TestLoginPostRunLokingHook(t *testing.T) { desc: "valid LDAP user", authUser: testUser, authModule: loginservice.LDAPAuthModule, - info: models.LoginInfo{ + info: loginservice.LoginInfo{ AuthModule: loginservice.LDAPAuthModule, User: testUser, HTTPStatus: 200, @@ -725,7 +726,7 @@ type fakeAuthenticator struct { ExpectedError error } -func (fa *fakeAuthenticator) AuthenticateUser(c context.Context, query *models.LoginUserQuery) error { +func (fa *fakeAuthenticator) AuthenticateUser(c context.Context, query *loginservice.LoginUserQuery) error { query.User = fa.ExpectedUser query.AuthModule = fa.ExpectedAuthModule return fa.ExpectedError diff --git a/pkg/api/metrics.go b/pkg/api/metrics.go index 6f74a46a3f9..695a28e21de 100644 --- a/pkg/api/metrics.go +++ b/pkg/api/metrics.go @@ -9,7 +9,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/web" @@ -46,7 +46,7 @@ func (hs *HTTPServer) handleQueryMetricsError(err error) *response.NormalRespons // 400: badRequestError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) QueryMetricsV2(c *models.ReqContext) response.Response { +func (hs *HTTPServer) QueryMetricsV2(c *contextmodel.ReqContext) response.Response { reqDTO := dtos.MetricRequest{} if err := web.Bind(c.Req, &reqDTO); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) diff --git a/pkg/api/openapi3.go b/pkg/api/openapi3.go index ad01675cf08..a5f3eb483a4 100644 --- a/pkg/api/openapi3.go +++ b/pkg/api/openapi3.go @@ -3,9 +3,9 @@ package api import ( "net/http" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" ) -func openapi3(c *models.ReqContext) { +func openapi3(c *contextmodel.ReqContext) { c.HTML(http.StatusOK, "openapi3", nil) } diff --git a/pkg/api/org.go b/pkg/api/org.go index 9030412153f..c4c2ea7a5d7 100644 --- a/pkg/api/org.go +++ b/pkg/api/org.go @@ -9,7 +9,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/metrics" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" @@ -25,7 +25,7 @@ import ( // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) GetCurrentOrg(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetCurrentOrg(c *contextmodel.ReqContext) response.Response { return hs.getOrgHelper(c.Req.Context(), c.OrgID) } @@ -41,7 +41,7 @@ func (hs *HTTPServer) GetCurrentOrg(c *models.ReqContext) response.Response { // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) GetOrgByID(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetOrgByID(c *contextmodel.ReqContext) response.Response { orgId, err := strconv.ParseInt(web.Params(c.Req)[":orgId"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "orgId is invalid", err) @@ -61,7 +61,7 @@ func (hs *HTTPServer) GetOrgByID(c *models.ReqContext) response.Response { // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) GetOrgByName(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetOrgByName(c *contextmodel.ReqContext) response.Response { orga, err := hs.orgService.GetByName(c.Req.Context(), &org.GetOrgByNameQuery{Name: web.Params(c.Req)[":name"]}) if err != nil { if errors.Is(err, org.ErrOrgNotFound) { @@ -126,7 +126,7 @@ func (hs *HTTPServer) getOrgHelper(ctx context.Context, orgID int64) response.Re // 403: forbiddenError // 409: conflictError // 500: internalServerError -func (hs *HTTPServer) CreateOrg(c *models.ReqContext) response.Response { +func (hs *HTTPServer) CreateOrg(c *contextmodel.ReqContext) response.Response { cmd := org.CreateOrgCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -163,7 +163,7 @@ func (hs *HTTPServer) CreateOrg(c *models.ReqContext) response.Response { // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) UpdateCurrentOrg(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UpdateCurrentOrg(c *contextmodel.ReqContext) response.Response { form := dtos.UpdateOrgForm{} if err := web.Bind(c.Req, &form); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -184,7 +184,7 @@ func (hs *HTTPServer) UpdateCurrentOrg(c *models.ReqContext) response.Response { // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) UpdateOrg(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UpdateOrg(c *contextmodel.ReqContext) response.Response { form := dtos.UpdateOrgForm{} if err := web.Bind(c.Req, &form); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -218,7 +218,7 @@ func (hs *HTTPServer) updateOrgHelper(ctx context.Context, form dtos.UpdateOrgFo // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) UpdateCurrentOrgAddress(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UpdateCurrentOrgAddress(c *contextmodel.ReqContext) response.Response { form := dtos.UpdateOrgAddressForm{} if err := web.Bind(c.Req, &form); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -236,7 +236,7 @@ func (hs *HTTPServer) UpdateCurrentOrgAddress(c *models.ReqContext) response.Res // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) UpdateOrgAddress(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UpdateOrgAddress(c *contextmodel.ReqContext) response.Response { form := dtos.UpdateOrgAddressForm{} if err := web.Bind(c.Req, &form); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -282,7 +282,7 @@ func (hs *HTTPServer) updateOrgAddressHelper(ctx context.Context, form dtos.Upda // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) DeleteOrgByID(c *models.ReqContext) response.Response { +func (hs *HTTPServer) DeleteOrgByID(c *contextmodel.ReqContext) response.Response { orgID, err := strconv.ParseInt(web.Params(c.Req)[":orgId"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "orgId is invalid", err) @@ -314,7 +314,7 @@ func (hs *HTTPServer) DeleteOrgByID(c *models.ReqContext) response.Response { // 403: forbiddenError // 409: conflictError // 500: internalServerError -func (hs *HTTPServer) SearchOrgs(c *models.ReqContext) response.Response { +func (hs *HTTPServer) SearchOrgs(c *contextmodel.ReqContext) response.Response { perPage := c.QueryInt("perpage") if perPage <= 0 { perPage = 1000 diff --git a/pkg/api/org_invite.go b/pkg/api/org_invite.go index 30271ed2f55..855c26c2922 100644 --- a/pkg/api/org_invite.go +++ b/pkg/api/org_invite.go @@ -12,8 +12,8 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/events" "github.com/grafana/grafana/pkg/infra/metrics" - "github.com/grafana/grafana/pkg/models" ac "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/services/org" tempuser "github.com/grafana/grafana/pkg/services/temp_user" @@ -32,7 +32,7 @@ import ( // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) GetPendingOrgInvites(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetPendingOrgInvites(c *contextmodel.ReqContext) response.Response { query := tempuser.GetTempUsersQuery{OrgID: c.OrgID, Status: tempuser.TmpUserInvitePending} queryResult, err := hs.tempUserService.GetTempUsersQuery(c.Req.Context(), &query) @@ -58,7 +58,7 @@ func (hs *HTTPServer) GetPendingOrgInvites(c *models.ReqContext) response.Respon // 403: forbiddenError // 412: SMTPNotEnabledError // 500: internalServerError -func (hs *HTTPServer) AddOrgInvite(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AddOrgInvite(c *contextmodel.ReqContext) response.Response { inviteDto := dtos.AddInviteForm{} if err := web.Bind(c.Req, &inviteDto); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -145,7 +145,7 @@ func (hs *HTTPServer) AddOrgInvite(c *models.ReqContext) response.Response { return response.Success(fmt.Sprintf("Created invite for %s", inviteDto.LoginOrEmail)) } -func (hs *HTTPServer) inviteExistingUserToOrg(c *models.ReqContext, user *user.User, inviteDto *dtos.AddInviteForm) response.Response { +func (hs *HTTPServer) inviteExistingUserToOrg(c *contextmodel.ReqContext, user *user.User, inviteDto *dtos.AddInviteForm) response.Response { // user exists, add org role createOrgUserCmd := org.AddOrgUserCommand{OrgID: c.OrgID, UserID: user.ID, Role: inviteDto.Role} if err := hs.orgService.AddOrgUser(c.Req.Context(), &createOrgUserCmd); err != nil { @@ -187,7 +187,7 @@ func (hs *HTTPServer) inviteExistingUserToOrg(c *models.ReqContext, user *user.U // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) RevokeInvite(c *models.ReqContext) response.Response { +func (hs *HTTPServer) RevokeInvite(c *contextmodel.ReqContext) response.Response { if ok, rsp := hs.updateTempUserStatus(c.Req.Context(), web.Params(c.Req)[":code"], tempuser.TmpUserRevoked); !ok { return rsp } @@ -198,7 +198,7 @@ func (hs *HTTPServer) RevokeInvite(c *models.ReqContext) response.Response { // GetInviteInfoByCode gets a pending user invite corresponding to a certain code. // A response containing an InviteInfo object is returned if the invite is found. // If a (pending) invite is not found, 404 is returned. -func (hs *HTTPServer) GetInviteInfoByCode(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetInviteInfoByCode(c *contextmodel.ReqContext) response.Response { query := tempuser.GetTempUserByCodeQuery{Code: web.Params(c.Req)[":code"]} queryResult, err := hs.tempUserService.GetTempUserByCode(c.Req.Context(), &query) if err != nil { @@ -221,7 +221,7 @@ func (hs *HTTPServer) GetInviteInfoByCode(c *models.ReqContext) response.Respons }) } -func (hs *HTTPServer) CompleteInvite(c *models.ReqContext) response.Response { +func (hs *HTTPServer) CompleteInvite(c *contextmodel.ReqContext) response.Response { completeInvite := dtos.CompleteInviteForm{} var err error if err = web.Bind(c.Req, &completeInvite); err != nil { diff --git a/pkg/api/org_invite_test.go b/pkg/api/org_invite_test.go index ed20e17894c..8b8829d8512 100644 --- a/pkg/api/org_invite_test.go +++ b/pkg/api/org_invite_test.go @@ -5,15 +5,15 @@ import ( "strings" "testing" - "github.com/grafana/grafana/pkg/services/org/orgtest" - "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/web/webtest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/services/accesscontrol" + "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" + "github.com/grafana/grafana/pkg/web/webtest" ) func TestOrgInvitesAPIEndpoint_RBAC(t *testing.T) { diff --git a/pkg/api/org_users.go b/pkg/api/org_users.go index a3151f2936b..b0c7c807cc0 100644 --- a/pkg/api/org_users.go +++ b/pkg/api/org_users.go @@ -9,8 +9,8 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" @@ -32,7 +32,7 @@ import ( // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) AddOrgUserToCurrentOrg(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AddOrgUserToCurrentOrg(c *contextmodel.ReqContext) response.Response { cmd := org.AddOrgUserCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -55,7 +55,7 @@ func (hs *HTTPServer) AddOrgUserToCurrentOrg(c *models.ReqContext) response.Resp // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) AddOrgUser(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AddOrgUser(c *contextmodel.ReqContext) response.Response { cmd := org.AddOrgUserCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -69,7 +69,7 @@ func (hs *HTTPServer) AddOrgUser(c *models.ReqContext) response.Response { return hs.addOrgUserHelper(c, cmd) } -func (hs *HTTPServer) addOrgUserHelper(c *models.ReqContext, cmd org.AddOrgUserCommand) response.Response { +func (hs *HTTPServer) addOrgUserHelper(c *contextmodel.ReqContext, cmd org.AddOrgUserCommand) response.Response { if !cmd.Role.IsValid() { return response.Error(400, "Invalid role specified", nil) } @@ -114,7 +114,7 @@ func (hs *HTTPServer) addOrgUserHelper(c *models.ReqContext, cmd org.AddOrgUserC // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) GetOrgUsersForCurrentOrg(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetOrgUsersForCurrentOrg(c *contextmodel.ReqContext) response.Response { result, err := hs.searchOrgUsersHelper(c, &org.SearchOrgUsersQuery{ OrgID: c.OrgID, Query: c.Query("query"), @@ -143,7 +143,7 @@ func (hs *HTTPServer) GetOrgUsersForCurrentOrg(c *models.ReqContext) response.Re // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) GetOrgUsersForCurrentOrgLookup(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetOrgUsersForCurrentOrgLookup(c *contextmodel.ReqContext) response.Response { orgUsersResult, err := hs.searchOrgUsersHelper(c, &org.SearchOrgUsersQuery{ OrgID: c.OrgID, Query: c.Query("query"), @@ -184,7 +184,7 @@ func (hs *HTTPServer) GetOrgUsersForCurrentOrgLookup(c *models.ReqContext) respo // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) GetOrgUsers(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetOrgUsers(c *contextmodel.ReqContext) response.Response { orgId, err := strconv.ParseInt(web.Params(c.Req)[":orgId"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "orgId is invalid", err) @@ -219,7 +219,7 @@ func (hs *HTTPServer) GetOrgUsers(c *models.ReqContext) response.Response { // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) SearchOrgUsers(c *models.ReqContext) response.Response { +func (hs *HTTPServer) SearchOrgUsers(c *contextmodel.ReqContext) response.Response { orgID, err := strconv.ParseInt(web.Params(c.Req)[":orgId"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "orgId is invalid", err) @@ -252,7 +252,7 @@ func (hs *HTTPServer) SearchOrgUsers(c *models.ReqContext) response.Response { // SearchOrgUsersWithPaging is an HTTP handler to search for org users with paging. // GET /api/org/users/search -func (hs *HTTPServer) SearchOrgUsersWithPaging(c *models.ReqContext) response.Response { +func (hs *HTTPServer) SearchOrgUsersWithPaging(c *contextmodel.ReqContext) response.Response { perPage := c.QueryInt("perpage") if perPage <= 0 { perPage = 1000 @@ -279,7 +279,7 @@ func (hs *HTTPServer) SearchOrgUsersWithPaging(c *models.ReqContext) response.Re return response.JSON(http.StatusOK, result) } -func (hs *HTTPServer) searchOrgUsersHelper(c *models.ReqContext, query *org.SearchOrgUsersQuery) (*org.SearchOrgUsersQueryResult, error) { +func (hs *HTTPServer) searchOrgUsersHelper(c *contextmodel.ReqContext, query *org.SearchOrgUsersQuery) (*org.SearchOrgUsersQueryResult, error) { result, err := hs.orgService.SearchOrgUsers(c.Req.Context(), query) if err != nil { return nil, err @@ -299,7 +299,7 @@ func (hs *HTTPServer) searchOrgUsersHelper(c *models.ReqContext, query *org.Sear filteredUsers = append(filteredUsers, user) } - modules, err := hs.authInfoService.GetUserLabels(c.Req.Context(), models.GetUserLabelsQuery{ + modules, err := hs.authInfoService.GetUserLabels(c.Req.Context(), login.GetUserLabelsQuery{ UserIDs: authLabelsUserIDs, }) @@ -335,7 +335,7 @@ func (hs *HTTPServer) searchOrgUsersHelper(c *models.ReqContext, query *org.Sear // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) UpdateOrgUserForCurrentOrg(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UpdateOrgUserForCurrentOrg(c *contextmodel.ReqContext) response.Response { cmd := org.UpdateOrgUserCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -362,7 +362,7 @@ func (hs *HTTPServer) UpdateOrgUserForCurrentOrg(c *models.ReqContext) response. // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) UpdateOrgUser(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UpdateOrgUser(c *contextmodel.ReqContext) response.Response { cmd := org.UpdateOrgUserCommand{} var err error if err := web.Bind(c.Req, &cmd); err != nil { @@ -379,7 +379,7 @@ func (hs *HTTPServer) UpdateOrgUser(c *models.ReqContext) response.Response { return hs.updateOrgUserHelper(c, cmd) } -func (hs *HTTPServer) updateOrgUserHelper(c *models.ReqContext, cmd org.UpdateOrgUserCommand) response.Response { +func (hs *HTTPServer) updateOrgUserHelper(c *contextmodel.ReqContext, cmd org.UpdateOrgUserCommand) response.Response { if !cmd.Role.IsValid() { return response.Error(400, "Invalid role specified", nil) } @@ -409,7 +409,7 @@ func (hs *HTTPServer) updateOrgUserHelper(c *models.ReqContext, cmd org.UpdateOr // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) RemoveOrgUserForCurrentOrg(c *models.ReqContext) response.Response { +func (hs *HTTPServer) RemoveOrgUserForCurrentOrg(c *contextmodel.ReqContext) response.Response { userId, err := strconv.ParseInt(web.Params(c.Req)[":userId"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "userId is invalid", err) @@ -435,7 +435,7 @@ func (hs *HTTPServer) RemoveOrgUserForCurrentOrg(c *models.ReqContext) response. // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) RemoveOrgUser(c *models.ReqContext) response.Response { +func (hs *HTTPServer) RemoveOrgUser(c *contextmodel.ReqContext) response.Response { userId, err := strconv.ParseInt(web.Params(c.Req)[":userId"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "userId is invalid", err) diff --git a/pkg/api/password.go b/pkg/api/password.go index 5d60de08254..80753bad9c7 100644 --- a/pkg/api/password.go +++ b/pkg/api/password.go @@ -8,6 +8,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/services/user" @@ -16,7 +17,7 @@ import ( "github.com/grafana/grafana/pkg/web" ) -func (hs *HTTPServer) SendResetPasswordEmail(c *models.ReqContext) response.Response { +func (hs *HTTPServer) SendResetPasswordEmail(c *contextmodel.ReqContext) response.Response { form := dtos.SendResetPasswordEmailForm{} if err := web.Bind(c.Req, &form); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -38,7 +39,7 @@ func (hs *HTTPServer) SendResetPasswordEmail(c *models.ReqContext) response.Resp return response.Error(http.StatusOK, "Email sent", nil) } - getAuthQuery := models.GetAuthInfoQuery{UserId: usr.ID} + getAuthQuery := login.GetAuthInfoQuery{UserId: usr.ID} if err := hs.authInfoService.GetAuthInfo(c.Req.Context(), &getAuthQuery); err == nil { authModule := getAuthQuery.Result.AuthModule if authModule == login.LDAPAuthModule || authModule == login.AuthProxyAuthModule { @@ -54,7 +55,7 @@ func (hs *HTTPServer) SendResetPasswordEmail(c *models.ReqContext) response.Resp return response.Success("Email sent") } -func (hs *HTTPServer) ResetPassword(c *models.ReqContext) response.Response { +func (hs *HTTPServer) ResetPassword(c *contextmodel.ReqContext) response.Response { form := dtos.ResetUserPasswordForm{} if err := web.Bind(c.Req, &form); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) diff --git a/pkg/api/playlist.go b/pkg/api/playlist.go index e37f4d5c577..b9303bff7b3 100644 --- a/pkg/api/playlist.go +++ b/pkg/api/playlist.go @@ -5,12 +5,12 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/playlist" "github.com/grafana/grafana/pkg/web" ) -func (hs *HTTPServer) ValidateOrgPlaylist(c *models.ReqContext) { +func (hs *HTTPServer) ValidateOrgPlaylist(c *contextmodel.ReqContext) { uid := web.Params(c.Req)[":uid"] query := playlist.GetPlaylistByUidQuery{UID: uid, OrgId: c.OrgID} p, err := hs.playlistService.GetWithoutItems(c.Req.Context(), &query) @@ -38,7 +38,7 @@ func (hs *HTTPServer) ValidateOrgPlaylist(c *models.ReqContext) { // Responses: // 200: searchPlaylistsResponse // 500: internalServerError -func (hs *HTTPServer) SearchPlaylists(c *models.ReqContext) response.Response { +func (hs *HTTPServer) SearchPlaylists(c *contextmodel.ReqContext) response.Response { query := c.Query("query") limit := c.QueryInt("limit") @@ -70,7 +70,7 @@ func (hs *HTTPServer) SearchPlaylists(c *models.ReqContext) response.Response { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetPlaylist(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetPlaylist(c *contextmodel.ReqContext) response.Response { uid := web.Params(c.Req)[":uid"] cmd := playlist.GetPlaylistByUidQuery{UID: uid, OrgId: c.OrgID} @@ -92,7 +92,7 @@ func (hs *HTTPServer) GetPlaylist(c *models.ReqContext) response.Response { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetPlaylistItems(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetPlaylistItems(c *contextmodel.ReqContext) response.Response { uid := web.Params(c.Req)[":uid"] cmd := playlist.GetPlaylistByUidQuery{UID: uid, OrgId: c.OrgID} @@ -114,7 +114,7 @@ func (hs *HTTPServer) GetPlaylistItems(c *models.ReqContext) response.Response { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetPlaylistDashboards(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetPlaylistDashboards(c *contextmodel.ReqContext) response.Response { playlistUID := web.Params(c.Req)[":uid"] playlists, err := hs.LoadPlaylistDashboards(c.Req.Context(), c.OrgID, c.SignedInUser, playlistUID) @@ -135,7 +135,7 @@ func (hs *HTTPServer) GetPlaylistDashboards(c *models.ReqContext) response.Respo // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) DeletePlaylist(c *models.ReqContext) response.Response { +func (hs *HTTPServer) DeletePlaylist(c *contextmodel.ReqContext) response.Response { uid := web.Params(c.Req)[":uid"] cmd := playlist.DeletePlaylistCommand{UID: uid, OrgId: c.OrgID} @@ -156,7 +156,7 @@ func (hs *HTTPServer) DeletePlaylist(c *models.ReqContext) response.Response { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) CreatePlaylist(c *models.ReqContext) response.Response { +func (hs *HTTPServer) CreatePlaylist(c *contextmodel.ReqContext) response.Response { cmd := playlist.CreatePlaylistCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -181,7 +181,7 @@ func (hs *HTTPServer) CreatePlaylist(c *models.ReqContext) response.Response { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) UpdatePlaylist(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UpdatePlaylist(c *contextmodel.ReqContext) response.Response { cmd := playlist.UpdatePlaylistCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) diff --git a/pkg/api/plugin_dashboards.go b/pkg/api/plugin_dashboards.go index 7710c41ca1e..4446b490825 100644 --- a/pkg/api/plugin_dashboards.go +++ b/pkg/api/plugin_dashboards.go @@ -5,8 +5,8 @@ import ( "net/http" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/plugindashboards" "github.com/grafana/grafana/pkg/web" ) @@ -14,7 +14,7 @@ import ( // GetPluginDashboards get plugin dashboards. // // /api/plugins/:pluginId/dashboards -func (hs *HTTPServer) GetPluginDashboards(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetPluginDashboards(c *contextmodel.ReqContext) response.Response { pluginID := web.Params(c.Req)[":pluginId"] listReq := &plugindashboards.ListPluginDashboardsRequest{ diff --git a/pkg/api/plugin_dashboards_test.go b/pkg/api/plugin_dashboards_test.go index 2abafcd5365..ae3709ba39c 100644 --- a/pkg/api/plugin_dashboards_test.go +++ b/pkg/api/plugin_dashboards_test.go @@ -9,13 +9,14 @@ import ( "net/http" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/plugindashboards" "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/web/webtest" - "github.com/stretchr/testify/require" ) func TestGetPluginDashboards(t *testing.T) { diff --git a/pkg/api/plugin_metrics.go b/pkg/api/plugin_metrics.go index 85379d05d52..e32a3ad29ab 100644 --- a/pkg/api/plugin_metrics.go +++ b/pkg/api/plugin_metrics.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/plugins/backendplugin" "github.com/grafana/grafana/pkg/web" ) diff --git a/pkg/api/plugin_metrics_test.go b/pkg/api/plugin_metrics_test.go index 03acced669c..40d4f6f3921 100644 --- a/pkg/api/plugin_metrics_test.go +++ b/pkg/api/plugin_metrics_test.go @@ -7,12 +7,13 @@ import ( "testing" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/backendplugin" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web/webtest" - "github.com/stretchr/testify/require" ) func TestPluginMetricsEndpoint(t *testing.T) { diff --git a/pkg/api/plugin_proxy.go b/pkg/api/plugin_proxy.go index 509c1f784bf..0ff09d45bb0 100644 --- a/pkg/api/plugin_proxy.go +++ b/pkg/api/plugin_proxy.go @@ -9,12 +9,12 @@ import ( "time" "github.com/grafana/grafana/pkg/api/pluginproxy" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/pluginsettings" "github.com/grafana/grafana/pkg/web" ) -func (hs *HTTPServer) ProxyPluginRequest(c *models.ReqContext) { +func (hs *HTTPServer) ProxyPluginRequest(c *contextmodel.ReqContext) { var once sync.Once var pluginProxyTransport *http.Transport once.Do(func() { @@ -63,6 +63,6 @@ func extractProxyPath(originalRawPath string) string { return pluginProxyPathRegexp.ReplaceAllString(originalRawPath, "") } -func getProxyPath(c *models.ReqContext) string { +func getProxyPath(c *contextmodel.ReqContext) string { return extractProxyPath(c.Req.URL.EscapedPath()) } diff --git a/pkg/api/plugin_resource.go b/pkg/api/plugin_resource.go index 970c4d6a4bc..a9b0ab61af0 100644 --- a/pkg/api/plugin_resource.go +++ b/pkg/api/plugin_resource.go @@ -12,8 +12,8 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins/backendplugin" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/util/proxyutil" "github.com/grafana/grafana/pkg/web" @@ -22,11 +22,11 @@ import ( // CallResource passes a resource call from a plugin to the backend plugin. // // /api/plugins/:pluginId/resources/* -func (hs *HTTPServer) CallResource(c *models.ReqContext) { +func (hs *HTTPServer) CallResource(c *contextmodel.ReqContext) { hs.callPluginResource(c, web.Params(c.Req)[":pluginId"]) } -func (hs *HTTPServer) callPluginResource(c *models.ReqContext, pluginID string) { +func (hs *HTTPServer) callPluginResource(c *contextmodel.ReqContext, pluginID string) { pCtx, found, err := hs.PluginContextProvider.Get(c.Req.Context(), pluginID, c.SignedInUser) if err != nil { c.JsonApiErr(500, "Failed to get plugin settings", err) @@ -48,7 +48,7 @@ func (hs *HTTPServer) callPluginResource(c *models.ReqContext, pluginID string) } } -func (hs *HTTPServer) callPluginResourceWithDataSource(c *models.ReqContext, pluginID string, ds *datasources.DataSource) { +func (hs *HTTPServer) callPluginResourceWithDataSource(c *contextmodel.ReqContext, pluginID string, ds *datasources.DataSource) { pCtx, found, err := hs.PluginContextProvider.GetWithDataSource(c.Req.Context(), pluginID, c.SignedInUser, ds) if err != nil { c.JsonApiErr(500, "Failed to get plugin settings", err) @@ -81,7 +81,7 @@ func (hs *HTTPServer) callPluginResourceWithDataSource(c *models.ReqContext, plu } } -func (hs *HTTPServer) pluginResourceRequest(c *models.ReqContext) (*http.Request, error) { +func (hs *HTTPServer) pluginResourceRequest(c *contextmodel.ReqContext) (*http.Request, error) { clonedReq := c.Req.Clone(c.Req.Context()) rawURL := web.Params(c.Req)["*"] if clonedReq.URL.RawQuery != "" { @@ -207,7 +207,7 @@ func (hs *HTTPServer) flushStream(stream callResourceClientResponseStream, w htt } } -func handleCallResourceError(err error, reqCtx *models.ReqContext) { +func handleCallResourceError(err error, reqCtx *contextmodel.ReqContext) { if errors.Is(err, backendplugin.ErrPluginUnavailable) { reqCtx.JsonApiErr(503, "Plugin unavailable", err) return diff --git a/pkg/api/plugin_resource_test.go b/pkg/api/plugin_resource_test.go index a7cf4e9bdfe..5b5944c8d3f 100644 --- a/pkg/api/plugin_resource_test.go +++ b/pkg/api/plugin_resource_test.go @@ -9,10 +9,9 @@ import ( "strings" "testing" - "github.com/stretchr/testify/require" - "github.com/grafana/grafana-azure-sdk-go/azsettings" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/localcache" @@ -23,10 +22,12 @@ import ( pluginClient "github.com/grafana/grafana/pkg/plugins/manager/client" "github.com/grafana/grafana/pkg/plugins/manager/fakes" "github.com/grafana/grafana/pkg/plugins/manager/loader" + "github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" "github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/manager/signature" "github.com/grafana/grafana/pkg/plugins/manager/store" "github.com/grafana/grafana/pkg/plugins/plugincontext" + "github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/services/accesscontrol" datasources "github.com/grafana/grafana/pkg/services/datasources/fakes" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -56,8 +57,9 @@ func TestCallResource(t *testing.T) { nil, nil, nil, nil, testdatasource.ProvideService(cfg, featuremgmt.WithFeatures()), nil, nil, nil, nil, nil, nil) pCfg := config.ProvideConfig(setting.ProvideProvider(cfg), cfg) reg := registry.ProvideService() + cdn := pluginscdn.ProvideService(pCfg) l := loader.ProvideService(pCfg, fakes.NewFakeLicensingService(), signature.NewUnsignedAuthorizer(pCfg), - reg, provider.ProvideService(coreRegistry), fakes.NewFakeRoleRegistry()) + reg, provider.ProvideService(coreRegistry), fakes.NewFakeRoleRegistry(), cdn, assetpath.ProvideService(cdn)) ps, err := store.ProvideService(cfg, pCfg, reg, l) require.NoError(t, err) diff --git a/pkg/api/pluginproxy/ds_proxy.go b/pkg/api/pluginproxy/ds_proxy.go index ba450c9f056..fa9cc95fa6b 100644 --- a/pkg/api/pluginproxy/ds_proxy.go +++ b/pkg/api/pluginproxy/ds_proxy.go @@ -17,8 +17,8 @@ import ( "github.com/grafana/grafana/pkg/infra/httpclient" glog "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/oauthtoken" "github.com/grafana/grafana/pkg/setting" @@ -33,7 +33,7 @@ var ( type DataSourceProxy struct { ds *datasources.DataSource - ctx *models.ReqContext + ctx *contextmodel.ReqContext targetUrl *url.URL proxyPath string matchedRoute *plugins.Route @@ -50,7 +50,7 @@ type httpClient interface { } // NewDataSourceProxy creates a new Datasource proxy -func NewDataSourceProxy(ds *datasources.DataSource, pluginRoutes []*plugins.Route, ctx *models.ReqContext, +func NewDataSourceProxy(ds *datasources.DataSource, pluginRoutes []*plugins.Route, ctx *contextmodel.ReqContext, proxyPath string, cfg *setting.Cfg, clientProvider httpclient.Provider, oAuthTokenService oauthtoken.OAuthTokenService, dsService datasources.DataSourceService, tracer tracing.Tracer) (*DataSourceProxy, error) { @@ -343,7 +343,7 @@ func (proxy *DataSourceProxy) logRequest() { "body", body) } -func checkWhiteList(c *models.ReqContext, host string) bool { +func checkWhiteList(c *contextmodel.ReqContext, host string) bool { if host != "" && len(setting.DataProxyWhiteList) > 0 { if _, exists := setting.DataProxyWhiteList[host]; !exists { c.JsonApiErr(403, "Data proxy hostname and ip are not included in whitelist", nil) diff --git a/pkg/api/pluginproxy/ds_proxy_test.go b/pkg/api/pluginproxy/ds_proxy_test.go index a552bb0ba5f..bf9fa018d77 100644 --- a/pkg/api/pluginproxy/ds_proxy_test.go +++ b/pkg/api/pluginproxy/ds_proxy_test.go @@ -25,12 +25,13 @@ import ( "github.com/grafana/grafana/pkg/infra/httpclient" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" datasourceservice "github.com/grafana/grafana/pkg/services/datasources/service" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/oauthtoken" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/quota/quotatest" @@ -128,10 +129,10 @@ func TestDataSourceProxy_routeRule(t *testing.T) { }, } - setUp := func() (*models.ReqContext, *http.Request) { + setUp := func() (*contextmodel.ReqContext, *http.Request) { req, err := http.NewRequest("GET", "http://localhost/asd", nil) require.NoError(t, err) - ctx := &models.ReqContext{ + ctx := &contextmodel.ReqContext{ Context: &web.Context{Req: req}, SignedInUser: &user.SignedInUser{OrgRole: org.RoleEditor}, } @@ -286,7 +287,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { req, err := http.NewRequest("GET", "http://localhost/asd", nil) require.NoError(t, err) - ctx := &models.ReqContext{ + ctx := &contextmodel.ReqContext{ Context: &web.Context{Req: req}, SignedInUser: &user.SignedInUser{OrgRole: org.RoleEditor}, } @@ -372,7 +373,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { t.Run("When proxying graphite", func(t *testing.T) { var routes []*plugins.Route ds := &datasources.DataSource{Url: "htttp://graphite:8080", Type: datasources.DS_GRAPHITE} - ctx := &models.ReqContext{} + ctx := &contextmodel.ReqContext{} sqlStore := db.InitTestDB(t) secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore()) @@ -401,7 +402,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { User: "user", } - ctx := &models.ReqContext{} + ctx := &contextmodel.ReqContext{} var routes []*plugins.Route sqlStore := db.InitTestDB(t) secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore()) @@ -429,7 +430,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { JsonData: json, } - ctx := &models.ReqContext{} + ctx := &contextmodel.ReqContext{} var routes []*plugins.Route sqlStore := db.InitTestDB(t) secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore()) @@ -461,7 +462,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { JsonData: json, } - ctx := &models.ReqContext{} + ctx := &contextmodel.ReqContext{} var pluginRoutes []*plugins.Route sqlStore := db.InitTestDB(t) secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore()) @@ -488,7 +489,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { Type: "custom-datasource", Url: "http://host/root/", } - ctx := &models.ReqContext{} + ctx := &contextmodel.ReqContext{} var routes []*plugins.Route sqlStore := db.InitTestDB(t) secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore()) @@ -522,7 +523,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { req, err := http.NewRequest("GET", "http://localhost/asd", nil) require.NoError(t, err) - ctx := &models.ReqContext{ + ctx := &contextmodel.ReqContext{ SignedInUser: &user.SignedInUser{UserID: 1}, Context: &web.Context{Req: req}, } @@ -563,7 +564,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { t.Run("When SendUserHeader config is enabled", func(t *testing.T) { req := getDatasourceProxiedRequest( t, - &models.ReqContext{ + &contextmodel.ReqContext{ SignedInUser: &user.SignedInUser{ Login: "test_user", }, @@ -576,7 +577,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { t.Run("When SendUserHeader config is disabled", func(t *testing.T) { req := getDatasourceProxiedRequest( t, - &models.ReqContext{ + &contextmodel.ReqContext{ SignedInUser: &user.SignedInUser{ Login: "test_user", }, @@ -590,7 +591,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { t.Run("When SendUserHeader config is enabled but user is anonymous", func(t *testing.T) { req := getDatasourceProxiedRequest( t, - &models.ReqContext{ + &contextmodel.ReqContext{ SignedInUser: &user.SignedInUser{IsAnonymous: true}, }, &setting.Cfg{SendUserHeader: true}, @@ -635,7 +636,7 @@ func TestDataSourceProxy_requestHandling(t *testing.T) { writeCb func(w http.ResponseWriter, r *http.Request) } - setUp := func(t *testing.T, cfgs ...setUpCfg) (*models.ReqContext, *datasources.DataSource) { + setUp := func(t *testing.T, cfgs ...setUpCfg) (*contextmodel.ReqContext, *datasources.DataSource) { writeErr = nil backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -668,7 +669,7 @@ func TestDataSourceProxy_requestHandling(t *testing.T) { } } - return &models.ReqContext{ + return &contextmodel.ReqContext{ SignedInUser: &user.SignedInUser{}, Context: &web.Context{ Req: httptest.NewRequest("GET", "/render", nil), @@ -822,7 +823,7 @@ func TestDataSourceProxy_requestHandling(t *testing.T) { } func TestNewDataSourceProxy_InvalidURL(t *testing.T) { - ctx := models.ReqContext{ + ctx := contextmodel.ReqContext{ Context: &web.Context{}, SignedInUser: &user.SignedInUser{OrgRole: org.RoleEditor}, } @@ -846,7 +847,7 @@ func TestNewDataSourceProxy_InvalidURL(t *testing.T) { } func TestNewDataSourceProxy_ProtocolLessURL(t *testing.T) { - ctx := models.ReqContext{ + ctx := contextmodel.ReqContext{ Context: &web.Context{}, SignedInUser: &user.SignedInUser{OrgRole: org.RoleEditor}, } @@ -871,7 +872,7 @@ func TestNewDataSourceProxy_ProtocolLessURL(t *testing.T) { // Test wth MSSQL type data sources. func TestNewDataSourceProxy_MSSQL(t *testing.T) { - ctx := models.ReqContext{ + ctx := contextmodel.ReqContext{ Context: &web.Context{}, SignedInUser: &user.SignedInUser{OrgRole: org.RoleEditor}, } @@ -926,7 +927,7 @@ func TestNewDataSourceProxy_MSSQL(t *testing.T) { } // getDatasourceProxiedRequest is a helper for easier setup of tests based on global config and ReqContext. -func getDatasourceProxiedRequest(t *testing.T, ctx *models.ReqContext, cfg *setting.Cfg) *http.Request { +func getDatasourceProxiedRequest(t *testing.T, ctx *contextmodel.ReqContext, cfg *setting.Cfg) *http.Request { ds := &datasources.DataSource{ Type: "custom", Url: "http://host/root/", @@ -1052,7 +1053,7 @@ func createAuthTest(t *testing.T, secretsStore secretskvs.SecretsKVStore, dsType } func runDatasourceAuthTest(t *testing.T, secretsService secrets.Service, secretsStore secretskvs.SecretsKVStore, cfg *setting.Cfg, test *testCase) { - ctx := &models.ReqContext{} + ctx := &contextmodel.ReqContext{} tracer := tracing.InitializeTracerForTest() var routes []*plugins.Route @@ -1089,10 +1090,10 @@ func Test_PathCheck(t *testing.T) { } tracer := tracing.InitializeTracerForTest() - setUp := func() (*models.ReqContext, *http.Request) { + setUp := func() (*contextmodel.ReqContext, *http.Request) { req, err := http.NewRequest("GET", "http://localhost/asd", nil) require.NoError(t, err) - ctx := &models.ReqContext{ + ctx := &contextmodel.ReqContext{ Context: &web.Context{Req: req}, SignedInUser: &user.SignedInUser{OrgRole: org.RoleViewer}, } @@ -1125,14 +1126,14 @@ func (m *mockOAuthTokenService) IsOAuthPassThruEnabled(ds *datasources.DataSourc return m.oAuthEnabled } -func (m *mockOAuthTokenService) HasOAuthEntry(context.Context, *user.SignedInUser) (*models.UserAuth, bool, error) { +func (m *mockOAuthTokenService) HasOAuthEntry(context.Context, *user.SignedInUser) (*login.UserAuth, bool, error) { return nil, false, nil } -func (m *mockOAuthTokenService) TryTokenRefresh(context.Context, *models.UserAuth) error { +func (m *mockOAuthTokenService) TryTokenRefresh(context.Context, *login.UserAuth) error { return nil } -func (m *mockOAuthTokenService) InvalidateOAuthTokens(context.Context, *models.UserAuth) error { +func (m *mockOAuthTokenService) InvalidateOAuthTokens(context.Context, *login.UserAuth) error { return nil } diff --git a/pkg/api/pluginproxy/pluginproxy.go b/pkg/api/pluginproxy/pluginproxy.go index e77b83d0cb3..a69abb209be 100644 --- a/pkg/api/pluginproxy/pluginproxy.go +++ b/pkg/api/pluginproxy/pluginproxy.go @@ -8,8 +8,8 @@ import ( "net/url" "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/pluginsettings" "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/setting" @@ -22,7 +22,7 @@ import ( type PluginProxy struct { ps *pluginsettings.DTO pluginRoutes []*plugins.Route - ctx *models.ReqContext + ctx *contextmodel.ReqContext proxyPath string matchedRoute *plugins.Route cfg *setting.Cfg @@ -32,7 +32,7 @@ type PluginProxy struct { } // NewPluginProxy creates a plugin proxy. -func NewPluginProxy(ps *pluginsettings.DTO, routes []*plugins.Route, ctx *models.ReqContext, +func NewPluginProxy(ps *pluginsettings.DTO, routes []*plugins.Route, ctx *contextmodel.ReqContext, proxyPath string, cfg *setting.Cfg, secretsService secrets.Service, tracer tracing.Tracer, transport *http.Transport) (*PluginProxy, error) { return &PluginProxy{ diff --git a/pkg/api/pluginproxy/pluginproxy_test.go b/pkg/api/pluginproxy/pluginproxy_test.go index d1e18072caa..4ac71cb9291 100644 --- a/pkg/api/pluginproxy/pluginproxy_test.go +++ b/pkg/api/pluginproxy/pluginproxy_test.go @@ -10,8 +10,8 @@ import ( "testing" "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/pluginsettings" "github.com/grafana/grafana/pkg/services/secrets" @@ -49,7 +49,7 @@ func TestPluginProxy(t *testing.T) { }, }, secretsService, - &models.ReqContext{ + &contextmodel.ReqContext{ SignedInUser: &user.SignedInUser{ Login: "test_user", }, @@ -72,7 +72,7 @@ func TestPluginProxy(t *testing.T) { t, &pluginsettings.DTO{}, secretsService, - &models.ReqContext{ + &contextmodel.ReqContext{ SignedInUser: &user.SignedInUser{ Login: "test_user", }, @@ -96,7 +96,7 @@ func TestPluginProxy(t *testing.T) { t, &pluginsettings.DTO{}, secretsService, - &models.ReqContext{ + &contextmodel.ReqContext{ SignedInUser: &user.SignedInUser{ Login: "test_user", }, @@ -119,7 +119,7 @@ func TestPluginProxy(t *testing.T) { t, &pluginsettings.DTO{}, secretsService, - &models.ReqContext{ + &contextmodel.ReqContext{ SignedInUser: &user.SignedInUser{IsAnonymous: true}, Context: &web.Context{ Req: httpReq, @@ -150,7 +150,7 @@ func TestPluginProxy(t *testing.T) { }, }, secretsService, - &models.ReqContext{ + &contextmodel.ReqContext{ SignedInUser: &user.SignedInUser{ Login: "test_user", }, @@ -178,7 +178,7 @@ func TestPluginProxy(t *testing.T) { t, &pluginsettings.DTO{}, secretsService, - &models.ReqContext{ + &contextmodel.ReqContext{ SignedInUser: &user.SignedInUser{ Login: "test_user", }, @@ -216,7 +216,7 @@ func TestPluginProxy(t *testing.T) { SecureJSONData: encryptedJsonData, }, secretsService, - &models.ReqContext{ + &contextmodel.ReqContext{ SignedInUser: &user.SignedInUser{ Login: "test_user", }, @@ -250,7 +250,7 @@ func TestPluginProxy(t *testing.T) { }, } - ctx := &models.ReqContext{ + ctx := &contextmodel.ReqContext{ SignedInUser: &user.SignedInUser{}, Context: &web.Context{ Req: httptest.NewRequest("GET", "/", nil), @@ -388,7 +388,7 @@ func TestPluginProxyRoutes(t *testing.T) { responseWriter := web.NewResponseWriter("GET", httptest.NewRecorder()) - ctx := &models.ReqContext{ + ctx := &contextmodel.ReqContext{ SignedInUser: &user.SignedInUser{}, Context: &web.Context{ Req: httptest.NewRequest("GET", tc.proxyPath, nil), @@ -420,7 +420,7 @@ func TestPluginProxyRoutes(t *testing.T) { } // getPluginProxiedRequest is a helper for easier setup of tests based on global config and ReqContext. -func getPluginProxiedRequest(t *testing.T, ps *pluginsettings.DTO, secretsService secrets.Service, ctx *models.ReqContext, cfg *setting.Cfg, route *plugins.Route) *http.Request { +func getPluginProxiedRequest(t *testing.T, ps *pluginsettings.DTO, secretsService secrets.Service, ctx *contextmodel.ReqContext, cfg *setting.Cfg, route *plugins.Route) *http.Request { // insert dummy route if none is specified if route == nil { route = &plugins.Route{ diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index 757c39fd999..2658391d2cc 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -15,15 +15,17 @@ import ( "strings" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/backendplugin" "github.com/grafana/grafana/pkg/plugins/repo" "github.com/grafana/grafana/pkg/plugins/storage" ac "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/org" @@ -33,7 +35,15 @@ import ( "github.com/grafana/grafana/pkg/web" ) -func (hs *HTTPServer) GetPluginList(c *models.ReqContext) response.Response { +// pluginsCDNFallbackRedirectRequests is a metric counter keeping track of how many +// requests are received on the plugins CDN backend redirect fallback handler. +var pluginsCDNFallbackRedirectRequests = promauto.NewCounterVec(prometheus.CounterOpts{ + Namespace: "grafana", + Name: "plugins_cdn_fallback_redirect_requests_total", + Help: "Number of requests to the plugins CDN backend redirect fallback handler.", +}, []string{"plugin_id", "plugin_version"}) + +func (hs *HTTPServer) GetPluginList(c *contextmodel.ReqContext) response.Response { typeFilter := c.Query("type") enabledFilter := c.Query("enabled") embeddedFilter := c.Query("embedded") @@ -158,7 +168,7 @@ func (hs *HTTPServer) GetPluginList(c *models.ReqContext) response.Response { return response.JSON(http.StatusOK, result) } -func (hs *HTTPServer) GetPluginSettingByID(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetPluginSettingByID(c *contextmodel.ReqContext) response.Response { pluginID := web.Params(c.Req)[":pluginId"] plugin, exists := hs.pluginStore.Plugin(c.Req.Context(), pluginID) @@ -227,7 +237,7 @@ func (hs *HTTPServer) GetPluginSettingByID(c *models.ReqContext) response.Respon return response.JSON(http.StatusOK, dto) } -func (hs *HTTPServer) UpdatePluginSetting(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UpdatePluginSetting(c *contextmodel.ReqContext) response.Response { cmd := pluginsettings.UpdatePluginSettingCmd{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -256,7 +266,7 @@ func (hs *HTTPServer) UpdatePluginSetting(c *models.ReqContext) response.Respons return response.Success("Plugin settings updated") } -func (hs *HTTPServer) GetPluginMarkdown(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetPluginMarkdown(c *contextmodel.ReqContext) response.Response { pluginID := web.Params(c.Req)[":pluginId"] name := web.Params(c.Req)[":name"] @@ -286,7 +296,7 @@ func (hs *HTTPServer) GetPluginMarkdown(c *models.ReqContext) response.Response // CollectPluginMetrics collect metrics from a plugin. // // /api/plugins/:pluginId/metrics -func (hs *HTTPServer) CollectPluginMetrics(c *models.ReqContext) response.Response { +func (hs *HTTPServer) CollectPluginMetrics(c *contextmodel.ReqContext) response.Response { pluginID := web.Params(c.Req)[":pluginId"] resp, err := hs.pluginClient.CollectMetrics(c.Req.Context(), &backend.CollectMetricsRequest{PluginContext: backend.PluginContext{PluginID: pluginID}}) if err != nil { @@ -301,8 +311,15 @@ func (hs *HTTPServer) CollectPluginMetrics(c *models.ReqContext) response.Respon // getPluginAssets returns public plugin assets (images, JS, etc.) // +// If the plugin has cdn = false in its config (default), it will always attempt to return the asset +// from the local filesystem. +// +// If the plugin has cdn = true and hs.Cfg.PluginsCDNURLTemplate is empty, it will get the file +// from the local filesystem. If hs.Cfg.PluginsCDNURLTemplate is not empty, +// this handler returns a redirect to the plugin asset file on the specified CDN. +// // /public/plugins/:pluginId/* -func (hs *HTTPServer) getPluginAssets(c *models.ReqContext) { +func (hs *HTTPServer) getPluginAssets(c *contextmodel.ReqContext) { pluginID := web.Params(c.Req)[":pluginId"] plugin, exists := hs.pluginStore.Plugin(c.Req.Context(), pluginID) if !exists { @@ -318,7 +335,19 @@ func (hs *HTTPServer) getPluginAssets(c *models.ReqContext) { return } - f, err := plugin.File(requestedFile) + if hs.pluginsCDNService.PluginSupported(pluginID) { + // Send a redirect to the client + hs.redirectCDNPluginAsset(c, plugin, requestedFile) + return + } + + // Send the actual file to the client from local filesystem + hs.serveLocalPluginAsset(c, plugin, requestedFile) +} + +// serveLocalPluginAsset returns the content of a plugin asset file from the local filesystem to the http client. +func (hs *HTTPServer) serveLocalPluginAsset(c *contextmodel.ReqContext, plugin plugins.PluginDTO, assetPath string) { + f, err := plugin.File(assetPath) if err != nil { if errors.Is(err, plugins.ErrFileNotExist) { c.JsonApiErr(404, "Plugin file not found", nil) @@ -346,20 +375,42 @@ func (hs *HTTPServer) getPluginAssets(c *models.ReqContext) { } if rs, ok := f.(io.ReadSeeker); ok { - http.ServeContent(c.Resp, c.Req, requestedFile, fi.ModTime(), rs) - } else { - b, err := io.ReadAll(f) - if err != nil { - c.JsonApiErr(500, "Plugin file exists but could not read", err) - return - } - http.ServeContent(c.Resp, c.Req, requestedFile, fi.ModTime(), bytes.NewReader(b)) + http.ServeContent(c.Resp, c.Req, assetPath, fi.ModTime(), rs) + return } + + b, err := io.ReadAll(f) + if err != nil { + c.JsonApiErr(500, "Plugin file exists but could not read", err) + return + } + http.ServeContent(c.Resp, c.Req, assetPath, fi.ModTime(), bytes.NewReader(b)) +} + +// redirectCDNPluginAsset redirects the http request to specified asset path on the configured plugins CDN. +func (hs *HTTPServer) redirectCDNPluginAsset(c *contextmodel.ReqContext, plugin plugins.PluginDTO, assetPath string) { + remoteURL, err := hs.pluginsCDNService.AssetURL(plugin.ID, plugin.Info.Version, assetPath) + if err != nil { + c.JsonApiErr(500, "Failed to get CDN plugin asset remote URL", err) + return + } + hs.log.Warn( + "plugin cdn redirect hit", + "pluginID", plugin.ID, + "pluginVersion", plugin.Info.Version, + "assetPath", assetPath, + "remoteURL", remoteURL, + ) + pluginsCDNFallbackRedirectRequests.With(prometheus.Labels{ + "plugin_id": plugin.ID, + "plugin_version": plugin.Info.Version, + }).Inc() + http.Redirect(c.Resp, c.Req, remoteURL, http.StatusTemporaryRedirect) } // CheckHealth returns the health of a plugin. // /api/plugins/:pluginId/health -func (hs *HTTPServer) CheckHealth(c *models.ReqContext) response.Response { +func (hs *HTTPServer) CheckHealth(c *contextmodel.ReqContext) response.Response { pluginID := web.Params(c.Req)[":pluginId"] pCtx, found, err := hs.PluginContextProvider.Get(c.Req.Context(), pluginID, c.SignedInUser) @@ -401,11 +452,11 @@ func (hs *HTTPServer) CheckHealth(c *models.ReqContext) response.Response { return response.JSON(http.StatusOK, payload) } -func (hs *HTTPServer) GetPluginErrorsList(_ *models.ReqContext) response.Response { +func (hs *HTTPServer) GetPluginErrorsList(_ *contextmodel.ReqContext) response.Response { return response.JSON(http.StatusOK, hs.pluginErrorResolver.PluginErrors()) } -func (hs *HTTPServer) InstallPlugin(c *models.ReqContext) response.Response { +func (hs *HTTPServer) InstallPlugin(c *contextmodel.ReqContext) response.Response { dto := dtos.InstallPluginCommand{} if err := web.Bind(c.Req, &dto); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -444,7 +495,7 @@ func (hs *HTTPServer) InstallPlugin(c *models.ReqContext) response.Response { return response.JSON(http.StatusOK, []byte{}) } -func (hs *HTTPServer) UninstallPlugin(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UninstallPlugin(c *contextmodel.ReqContext) response.Response { pluginID := web.Params(c.Req)[":pluginId"] err := hs.pluginInstaller.Remove(c.Req.Context(), pluginID) diff --git a/pkg/api/plugins_test.go b/pkg/api/plugins_test.go index 7f2c783a518..43765e56f51 100644 --- a/pkg/api/plugins_test.go +++ b/pkg/api/plugins_test.go @@ -12,17 +12,19 @@ import ( "strings" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/prometheus/client_golang/prometheus" + dto "github.com/prometheus/client_model/go" + "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log/logtest" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/config" + "github.com/grafana/grafana/pkg/plugins/pluginscdn" ac "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgtest" "github.com/grafana/grafana/pkg/services/pluginsettings" @@ -139,7 +141,7 @@ func Test_PluginsInstallAndUninstall_AccessControl(t *testing.T) { req := webtest.RequestWithSignedInUser(server.NewPostRequest("/api/plugins/test/install", input), userWithPermissions(1, tc.permissions)) res, err := server.SendJSON(req) require.NoError(t, err) - assert.Equal(t, tc.expectedCode, res.StatusCode) + require.Equal(t, tc.expectedCode, res.StatusCode) require.NoError(t, res.Body.Close()) }) @@ -148,12 +150,105 @@ func Test_PluginsInstallAndUninstall_AccessControl(t *testing.T) { req := webtest.RequestWithSignedInUser(server.NewPostRequest("/api/plugins/test/uninstall", input), userWithPermissions(1, tc.permissions)) res, err := server.SendJSON(req) require.NoError(t, err) - assert.Equal(t, tc.expectedCode, res.StatusCode) + require.Equal(t, tc.expectedCode, res.StatusCode) require.NoError(t, res.Body.Close()) }) } } +func Test_GetPluginAssetCDNRedirect(t *testing.T) { + const cdnPluginID = "cdn-plugin" + const nonCDNPluginID = "non-cdn-plugin" + t.Run("Plugin CDN asset redirect", func(t *testing.T) { + cdnPlugin := &plugins.Plugin{ + JSONData: plugins.JSONData{ID: cdnPluginID, Info: plugins.Info{Version: "1.0.0"}}, + } + nonCdnPlugin := &plugins.Plugin{ + JSONData: plugins.JSONData{ID: nonCDNPluginID, Info: plugins.Info{Version: "2.0.0"}}, + } + service := &plugins.FakePluginStore{ + PluginList: []plugins.PluginDTO{ + cdnPlugin.ToDTO(), + nonCdnPlugin.ToDTO(), + }, + } + cfg := setting.NewCfg() + cfg.PluginsCDNURLTemplate = "https://cdn.example.com/{id}/{version}/public/plugins/{id}/{assetPath}" + cfg.PluginSettings = map[string]map[string]string{ + cdnPluginID: {"cdn": "true"}, + } + + const cdnFolderBaseURL = "https://cdn.example.com/cdn-plugin/1.0.0/public/plugins/cdn-plugin" + + type tc struct { + assetURL string + expRelativeURL string + } + for _, cas := range []tc{ + {"module.js", "module.js"}, + {"other/folder/file.js", "other/folder/file.js"}, + {"double////slashes/file.js", "double/slashes/file.js"}, + } { + pluginAssetScenario( + t, + "When calling GET for a CDN plugin on", + fmt.Sprintf("/public/plugins/%s/%s", cdnPluginID, cas.assetURL), + "/public/plugins/:pluginId/*", + cfg, service, func(sc *scenarioContext) { + // Get the prometheus metric (to test that the handler is instrumented correctly) + counter := pluginsCDNFallbackRedirectRequests.With(prometheus.Labels{ + "plugin_id": cdnPluginID, + "plugin_version": "1.0.0", + }) + + // Encode the prometheus metric and get its value + var m dto.Metric + require.NoError(t, counter.Write(&m)) + before := m.Counter.GetValue() + + // Call handler + callGetPluginAsset(sc) + + // Check redirect code + location + require.Equal(t, http.StatusTemporaryRedirect, sc.resp.Code, "wrong status code") + require.Equal(t, cdnFolderBaseURL+"/"+cas.expRelativeURL, sc.resp.Header().Get("Location"), "wrong location header") + + // Check metric + require.NoError(t, counter.Write(&m)) + require.Equal(t, before+1, m.Counter.GetValue(), "prometheus metric not incremented") + }, + ) + } + pluginAssetScenario( + t, + "When calling GET for a non-CDN plugin on", + fmt.Sprintf("/public/plugins/%s/%s", nonCDNPluginID, "module.js"), + "/public/plugins/:pluginId/*", + cfg, service, func(sc *scenarioContext) { + // Here the metric should not increment + var m dto.Metric + counter := pluginsCDNFallbackRedirectRequests.With(prometheus.Labels{ + "plugin_id": nonCDNPluginID, + "plugin_version": "2.0.0", + }) + require.NoError(t, counter.Write(&m)) + require.Zero(t, m.Counter.GetValue()) + + // Call handler + callGetPluginAsset(sc) + + // 404 implies access to fs + require.Equal(t, http.StatusNotFound, sc.resp.Code) + require.Empty(t, sc.resp.Header().Get("Location")) + + // Ensure the metric did not change + require.NoError(t, counter.Write(&m)) + require.Zero(t, m.Counter.GetValue()) + }, + ) + }) +} + func Test_GetPluginAssets(t *testing.T) { pluginID := "test-plugin" pluginDir := "." @@ -185,8 +280,8 @@ func Test_GetPluginAssets(t *testing.T) { } url := fmt.Sprintf("/public/plugins/%s/%s", pluginID, requestedFile) - pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*", service, - func(sc *scenarioContext) { + pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*", + setting.NewCfg(), service, func(sc *scenarioContext) { callGetPluginAsset(sc) require.Equal(t, 200, sc.resp.Code) @@ -201,8 +296,8 @@ func Test_GetPluginAssets(t *testing.T) { } url := fmt.Sprintf("/public/plugins/%s/%s", pluginID, tmpFileInParentDir.Name()) - pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*", service, - func(sc *scenarioContext) { + pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*", + setting.NewCfg(), service, func(sc *scenarioContext) { callGetPluginAsset(sc) require.Equal(t, 404, sc.resp.Code) @@ -217,8 +312,8 @@ func Test_GetPluginAssets(t *testing.T) { requestedFile := "nonExistent" url := fmt.Sprintf("/public/plugins/%s/%s", pluginID, requestedFile) - pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*", service, - func(sc *scenarioContext) { + pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*", + setting.NewCfg(), service, func(sc *scenarioContext) { callGetPluginAsset(sc) var respJson map[string]interface{} @@ -237,8 +332,8 @@ func Test_GetPluginAssets(t *testing.T) { requestedFile := "nonExistent" url := fmt.Sprintf("/public/plugins/%s/%s", pluginID, requestedFile) - pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*", service, - func(sc *scenarioContext) { + pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*", + setting.NewCfg(), service, func(sc *scenarioContext) { callGetPluginAsset(sc) var respJson map[string]interface{} @@ -262,8 +357,8 @@ func Test_GetPluginAssets(t *testing.T) { l := &logtest.Fake{} url := fmt.Sprintf("/public/plugins/%s/%s", pluginID, requestedFile) - pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*", service, - func(sc *scenarioContext) { + pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*", + setting.NewCfg(), service, func(sc *scenarioContext) { callGetPluginAsset(sc) require.Equal(t, 200, sc.resp.Code) @@ -383,16 +478,22 @@ func callGetPluginAsset(sc *scenarioContext) { sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() } -func pluginAssetScenario(t *testing.T, desc string, url string, urlPattern string, pluginStore plugins.Store, - fn scenarioFunc) { +func pluginAssetScenario(t *testing.T, desc string, url string, urlPattern string, + cfg *setting.Cfg, pluginStore plugins.Store, fn scenarioFunc) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { + cfg.IsFeatureToggleEnabled = func(_ string) bool { return false } hs := HTTPServer{ - Cfg: setting.NewCfg(), + Cfg: cfg, pluginStore: pluginStore, + log: log.NewNopLogger(), + pluginsCDNService: pluginscdn.ProvideService(&config.Cfg{ + PluginsCDNURLTemplate: cfg.PluginsCDNURLTemplate, + PluginSettings: cfg.PluginSettings, + }), } sc := setupScenarioContext(t, url) - sc.defaultHandler = func(c *models.ReqContext) { + sc.defaultHandler = func(c *contextmodel.ReqContext) { sc.context = c hs.getPluginAssets(c) } diff --git a/pkg/api/preferences.go b/pkg/api/preferences.go index 456b5a31dc6..9ec93d64631 100644 --- a/pkg/api/preferences.go +++ b/pkg/api/preferences.go @@ -7,7 +7,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/kinds/preferences" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" pref "github.com/grafana/grafana/pkg/services/preference" "github.com/grafana/grafana/pkg/web" @@ -17,10 +17,11 @@ const ( defaultTheme string = "" darkTheme string = "dark" lightTheme string = "light" + systemTheme string = "system" ) // POST /api/preferences/set-home-dash -func (hs *HTTPServer) SetHomeDashboard(c *models.ReqContext) response.Response { +func (hs *HTTPServer) SetHomeDashboard(c *contextmodel.ReqContext) response.Response { cmd := pref.SavePreferenceCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -61,7 +62,7 @@ func (hs *HTTPServer) SetHomeDashboard(c *models.ReqContext) response.Response { // 200: getPreferencesResponse // 401: unauthorisedError // 500: internalServerError -func (hs *HTTPServer) GetUserPreferences(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetUserPreferences(c *contextmodel.ReqContext) response.Response { return hs.getPreferencesFor(c.Req.Context(), c.OrgID, c.UserID, 0) } @@ -125,7 +126,7 @@ func (hs *HTTPServer) getPreferencesFor(ctx context.Context, orgID, userID, team // 400: badRequestError // 401: unauthorisedError // 500: internalServerError -func (hs *HTTPServer) UpdateUserPreferences(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UpdateUserPreferences(c *contextmodel.ReqContext) response.Response { dtoCmd := dtos.UpdatePrefsCmd{} if err := web.Bind(c.Req, &dtoCmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -134,7 +135,7 @@ func (hs *HTTPServer) UpdateUserPreferences(c *models.ReqContext) response.Respo } func (hs *HTTPServer) updatePreferencesFor(ctx context.Context, orgID, userID, teamId int64, dtoCmd *dtos.UpdatePrefsCmd) response.Response { - if dtoCmd.Theme != lightTheme && dtoCmd.Theme != darkTheme && dtoCmd.Theme != defaultTheme { + if dtoCmd.Theme != lightTheme && dtoCmd.Theme != darkTheme && dtoCmd.Theme != defaultTheme && dtoCmd.Theme != systemTheme { return response.Error(400, "Invalid theme", nil) } @@ -182,7 +183,7 @@ func (hs *HTTPServer) updatePreferencesFor(ctx context.Context, orgID, userID, t // 400: badRequestError // 401: unauthorisedError // 500: internalServerError -func (hs *HTTPServer) PatchUserPreferences(c *models.ReqContext) response.Response { +func (hs *HTTPServer) PatchUserPreferences(c *contextmodel.ReqContext) response.Response { dtoCmd := dtos.PatchPrefsCmd{} if err := web.Bind(c.Req, &dtoCmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -191,7 +192,7 @@ func (hs *HTTPServer) PatchUserPreferences(c *models.ReqContext) response.Respon } func (hs *HTTPServer) patchPreferencesFor(ctx context.Context, orgID, userID, teamId int64, dtoCmd *dtos.PatchPrefsCmd) response.Response { - if dtoCmd.Theme != nil && *dtoCmd.Theme != lightTheme && *dtoCmd.Theme != darkTheme && *dtoCmd.Theme != defaultTheme { + if dtoCmd.Theme != nil && *dtoCmd.Theme != lightTheme && *dtoCmd.Theme != darkTheme && *dtoCmd.Theme != defaultTheme && *dtoCmd.Theme != systemTheme { return response.Error(400, "Invalid theme", nil) } @@ -241,7 +242,7 @@ func (hs *HTTPServer) patchPreferencesFor(ctx context.Context, orgID, userID, te // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) GetOrgPreferences(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetOrgPreferences(c *contextmodel.ReqContext) response.Response { return hs.getPreferencesFor(c.Req.Context(), c.OrgID, 0, 0) } @@ -255,7 +256,7 @@ func (hs *HTTPServer) GetOrgPreferences(c *models.ReqContext) response.Response // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) UpdateOrgPreferences(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UpdateOrgPreferences(c *contextmodel.ReqContext) response.Response { dtoCmd := dtos.UpdatePrefsCmd{} if err := web.Bind(c.Req, &dtoCmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -274,7 +275,7 @@ func (hs *HTTPServer) UpdateOrgPreferences(c *models.ReqContext) response.Respon // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) PatchOrgPreferences(c *models.ReqContext) response.Response { +func (hs *HTTPServer) PatchOrgPreferences(c *contextmodel.ReqContext) response.Response { dtoCmd := dtos.PatchPrefsCmd{} if err := web.Bind(c.Req, &dtoCmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) diff --git a/pkg/api/quota.go b/pkg/api/quota.go index dd0ee9f538d..d712e55824a 100644 --- a/pkg/api/quota.go +++ b/pkg/api/quota.go @@ -5,7 +5,7 @@ import ( "strconv" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/web" ) @@ -22,7 +22,7 @@ import ( // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetCurrentOrgQuotas(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetCurrentOrgQuotas(c *contextmodel.ReqContext) response.Response { return hs.getOrgQuotasHelper(c, c.OrgID) } @@ -38,7 +38,7 @@ func (hs *HTTPServer) GetCurrentOrgQuotas(c *models.ReqContext) response.Respons // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetOrgQuotas(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetOrgQuotas(c *contextmodel.ReqContext) response.Response { orgId, err := strconv.ParseInt(web.Params(c.Req)[":orgId"], 10, 64) if err != nil { return response.Err(quota.ErrBadRequest.Errorf("orgId is invalid: %w", err)) @@ -46,7 +46,7 @@ func (hs *HTTPServer) GetOrgQuotas(c *models.ReqContext) response.Response { return hs.getOrgQuotasHelper(c, orgId) } -func (hs *HTTPServer) getOrgQuotasHelper(c *models.ReqContext, orgID int64) response.Response { +func (hs *HTTPServer) getOrgQuotasHelper(c *contextmodel.ReqContext, orgID int64) response.Response { q, err := hs.QuotaService.GetQuotasByScope(c.Req.Context(), quota.OrgScope, orgID) if err != nil { return response.ErrOrFallback(http.StatusInternalServerError, "failed to get quota", err) @@ -69,7 +69,7 @@ func (hs *HTTPServer) getOrgQuotasHelper(c *models.ReqContext, orgID int64) resp // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) UpdateOrgQuota(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UpdateOrgQuota(c *contextmodel.ReqContext) response.Response { cmd := quota.UpdateQuotaCmd{} var err error if err := web.Bind(c.Req, &cmd); err != nil { @@ -113,7 +113,7 @@ func (hs *HTTPServer) UpdateOrgQuota(c *models.ReqContext) response.Response { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetUserQuotas(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetUserQuotas(c *contextmodel.ReqContext) response.Response { id, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64) if err != nil { return response.Err(quota.ErrBadRequest.Errorf("id is invalid: %w", err)) @@ -142,7 +142,7 @@ func (hs *HTTPServer) GetUserQuotas(c *models.ReqContext) response.Response { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) UpdateUserQuota(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UpdateUserQuota(c *contextmodel.ReqContext) response.Response { cmd := quota.UpdateQuotaCmd{} var err error if err := web.Bind(c.Req, &cmd); err != nil { diff --git a/pkg/api/render.go b/pkg/api/render.go index 6b2c3480998..de80fbf8db5 100644 --- a/pkg/api/render.go +++ b/pkg/api/render.go @@ -8,12 +8,13 @@ import ( "time" "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/web" ) -func (hs *HTTPServer) RenderToPng(c *models.ReqContext) { +func (hs *HTTPServer) RenderToPng(c *contextmodel.ReqContext) { queryReader, err := util.NewURLQueryReader(c.Req.URL) if err != nil { c.Handle(hs.Cfg, 400, "Render parameters error", err) diff --git a/pkg/api/response/response.go b/pkg/api/response/response.go index 744a8f0ab45..aa8839d169f 100644 --- a/pkg/api/response/response.go +++ b/pkg/api/response/response.go @@ -9,9 +9,10 @@ import ( "reflect" jsoniter "github.com/json-iterator/go" + "gopkg.in/yaml.v3" "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util/errutil" ) @@ -19,7 +20,7 @@ import ( // Response is an HTTP response interface. type Response interface { // WriteTo writes to a context. - WriteTo(ctx *models.ReqContext) + WriteTo(ctx *contextmodel.ReqContext) // Body gets the response's body. Body() []byte // Status gets the response's status. @@ -77,7 +78,7 @@ func (r *NormalResponse) ErrMessage() string { return r.errMessage } -func (r *NormalResponse) WriteTo(ctx *models.ReqContext) { +func (r *NormalResponse) WriteTo(ctx *contextmodel.ReqContext) { if r.err != nil { v := map[string]interface{}{} traceID := tracing.TraceIDFromContext(ctx.Req.Context(), false) @@ -132,7 +133,7 @@ func (r StreamingResponse) Body() []byte { // WriteTo writes the response to the provided context. // Required to implement api.Response. -func (r StreamingResponse) WriteTo(ctx *models.ReqContext) { +func (r StreamingResponse) WriteTo(ctx *contextmodel.ReqContext) { header := ctx.Resp.Header() for k, v := range r.header { header[k] = v @@ -155,7 +156,7 @@ type RedirectResponse struct { } // WriteTo writes to a response. -func (r *RedirectResponse) WriteTo(ctx *models.ReqContext) { +func (r *RedirectResponse) WriteTo(ctx *contextmodel.ReqContext) { ctx.Redirect(r.location) } @@ -173,7 +174,8 @@ func (r *RedirectResponse) Body() []byte { // JSON creates a JSON response. func JSON(status int, body interface{}) *NormalResponse { - return Respond(status, body).SetHeader("Content-Type", "application/json") + return Respond(status, body). + SetHeader("Content-Type", "application/json") } // JSONStreaming creates a streaming JSON response. @@ -187,6 +189,30 @@ func JSONStreaming(status int, body interface{}) StreamingResponse { } } +// JSONDownload creates a JSON response indicating that it should be downloaded. +func JSONDownload(status int, body interface{}, filename string) *NormalResponse { + return JSON(status, body). + SetHeader("Content-Disposition", fmt.Sprintf(`attachment;filename="%s"`, filename)) +} + +// YAML creates a YAML response. +func YAML(status int, body interface{}) *NormalResponse { + b, err := yaml.Marshal(body) + if err != nil { + return Error(http.StatusInternalServerError, "body yaml marshal", err) + } + // As of now, application/yaml is downloaded by default in chrome regardless of Content-Disposition, so we use text/yaml instead. + return Respond(status, b). + SetHeader("Content-Type", "text/yaml") +} + +// YAMLDownload creates a YAML response indicating that it should be downloaded. +func YAMLDownload(status int, body interface{}, filename string) *NormalResponse { + return YAML(status, body). + SetHeader("Content-Type", "application/yaml"). + SetHeader("Content-Disposition", fmt.Sprintf(`attachment;filename="%s"`, filename)) +} + // Success create a successful response func Success(message string) *NormalResponse { resp := make(map[string]interface{}) diff --git a/pkg/api/response/response_test.go b/pkg/api/response/response_test.go index 67658cb497a..a100c42ae48 100644 --- a/pkg/api/response/response_test.go +++ b/pkg/api/response/response_test.go @@ -5,10 +5,10 @@ import ( "net/http" "testing" - "github.com/grafana/grafana/pkg/util/errutil" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/util/errutil" ) func TestErrors(t *testing.T) { diff --git a/pkg/api/response/web_hack.go b/pkg/api/response/web_hack.go index 81f3bfa4ac5..52aa9ee38a7 100644 --- a/pkg/api/response/web_hack.go +++ b/pkg/api/response/web_hack.go @@ -7,17 +7,17 @@ import ( "fmt" "net/http" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/web" ) type ( handlerStd = func(http.ResponseWriter, *http.Request) handlerStdCtx = func(http.ResponseWriter, *http.Request, *web.Context) - handlerStdReqCtx = func(http.ResponseWriter, *http.Request, *models.ReqContext) - handlerReqCtx = func(*models.ReqContext) - handlerReqCtxRes = func(*models.ReqContext) Response + handlerStdReqCtx = func(http.ResponseWriter, *http.Request, *contextmodel.ReqContext) + handlerReqCtx = func(*contextmodel.ReqContext) + handlerReqCtxRes = func(*contextmodel.ReqContext) Response handlerCtx = func(*web.Context) ) @@ -67,11 +67,11 @@ func webCtx(w http.ResponseWriter, r *http.Request) *web.Context { return ctx } -func reqCtx(w http.ResponseWriter, r *http.Request) *models.ReqContext { +func reqCtx(w http.ResponseWriter, r *http.Request) *contextmodel.ReqContext { wCtx := webCtx(w, r) - reqCtx, ok := wCtx.Req.Context().Value(ctxkey.Key{}).(*models.ReqContext) + reqCtx, ok := wCtx.Req.Context().Value(ctxkey.Key{}).(*contextmodel.ReqContext) if !ok { - panic("no *models.ReqContext found") + panic("no *contextmodel.ReqContext found") } return reqCtx } diff --git a/pkg/api/routing/routing.go b/pkg/api/routing/routing.go index 204ede8bf56..c5f5ec09759 100644 --- a/pkg/api/routing/routing.go +++ b/pkg/api/routing/routing.go @@ -2,7 +2,7 @@ package routing import ( "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/web" ) @@ -12,8 +12,8 @@ var ( } ) -func Wrap(handler func(c *models.ReqContext) response.Response) web.Handler { - return func(c *models.ReqContext) { +func Wrap(handler func(c *contextmodel.ReqContext) response.Response) web.Handler { + return func(c *contextmodel.ReqContext) { if res := handler(c); res != nil { res.WriteTo(c) } diff --git a/pkg/api/search.go b/pkg/api/search.go index 5ebf630f606..0eb478fc05a 100644 --- a/pkg/api/search.go +++ b/pkg/api/search.go @@ -6,10 +6,11 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/metrics" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/search" + "github.com/grafana/grafana/pkg/services/search/model" "github.com/grafana/grafana/pkg/util" ) @@ -20,7 +21,7 @@ import ( // 401: unauthorisedError // 422: unprocessableEntityError // 500: internalServerError -func (hs *HTTPServer) Search(c *models.ReqContext) response.Response { +func (hs *HTTPServer) Search(c *contextmodel.ReqContext) response.Response { query := c.Query("query") tags := c.QueryStrings("tag") starred := c.Query("starred") @@ -94,12 +95,12 @@ func (hs *HTTPServer) Search(c *models.ReqContext) response.Response { return hs.searchHitsWithMetadata(c, searchQuery.Result) } -func (hs *HTTPServer) searchHitsWithMetadata(c *models.ReqContext, hits models.HitList) response.Response { +func (hs *HTTPServer) searchHitsWithMetadata(c *contextmodel.ReqContext, hits model.HitList) response.Response { folderUIDs := make(map[string]bool) dashboardUIDs := make(map[string]bool) for _, hit := range hits { - if hit.Type == models.DashHitFolder { + if hit.Type == model.DashHitFolder { folderUIDs[hit.UID] = true } else { dashboardUIDs[hit.UID] = true @@ -112,13 +113,13 @@ func (hs *HTTPServer) searchHitsWithMetadata(c *models.ReqContext, hits models.H // search hit with access control metadata attached type hitWithMeta struct { - *models.Hit + *model.Hit AccessControl accesscontrol.Metadata `json:"accessControl,omitempty"` } hitsWithMeta := make([]hitWithMeta, 0, len(hits)) for _, hit := range hits { var meta accesscontrol.Metadata - if hit.Type == models.DashHitFolder { + if hit.Type == model.DashHitFolder { meta = folderMeta[hit.UID] } else { meta = accesscontrol.MergeMeta("dashboards", dashboardMeta[hit.UID], folderMeta[hit.FolderUID]) @@ -136,7 +137,7 @@ func (hs *HTTPServer) searchHitsWithMetadata(c *models.ReqContext, hits models.H // Responses: // 200: listSortOptionsResponse // 401: unauthorisedError -func (hs *HTTPServer) ListSortOptions(c *models.ReqContext) response.Response { +func (hs *HTTPServer) ListSortOptions(c *contextmodel.ReqContext) response.Response { opts := hs.SearchService.SortOptions() res := []util.DynMap{} @@ -215,7 +216,7 @@ type SearchParams struct { // swagger:response searchResponse type SearchResponse struct { // in: body - Body models.HitList `json:"body"` + Body model.HitList `json:"body"` } // swagger:response listSortOptionsResponse diff --git a/pkg/api/search_test.go b/pkg/api/search_test.go index 49ffef06275..c2d70b45dc3 100644 --- a/pkg/api/search_test.go +++ b/pkg/api/search_test.go @@ -9,9 +9,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/search/model" "github.com/grafana/grafana/pkg/services/user" ) @@ -21,10 +21,10 @@ func TestHTTPServer_Search(t *testing.T) { sc.initCtx.SignedInUser = &user.SignedInUser{} sc.hs.SearchService = &mockSearchService{ - ExpectedResult: models.HitList{ - {ID: 1, UID: "folder1", Title: "folder1", Type: models.DashHitFolder}, - {ID: 2, UID: "folder2", Title: "folder2", Type: models.DashHitFolder}, - {ID: 3, UID: "dash3", Title: "dash3", FolderUID: "folder2", Type: models.DashHitDB}, + ExpectedResult: model.HitList{ + {ID: 1, UID: "folder1", Title: "folder1", Type: model.DashHitFolder}, + {ID: 2, UID: "folder2", Title: "folder2", Type: model.DashHitFolder}, + {ID: 3, UID: "dash3", Title: "dash3", FolderUID: "folder2", Type: model.DashHitDB}, }, } @@ -38,7 +38,7 @@ func TestHTTPServer_Search(t *testing.T) { } type withMeta struct { - models.Hit + model.Hit AccessControl accesscontrol.Metadata `json:"accessControl,omitempty"` } diff --git a/pkg/api/short_url.go b/pkg/api/short_url.go index 040818547ae..f3ff7a6a8db 100644 --- a/pkg/api/short_url.go +++ b/pkg/api/short_url.go @@ -7,7 +7,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/shorturls" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" @@ -15,7 +15,7 @@ import ( ) // createShortURL handles requests to create short URLs. -func (hs *HTTPServer) createShortURL(c *models.ReqContext) response.Response { +func (hs *HTTPServer) createShortURL(c *contextmodel.ReqContext) response.Response { cmd := dtos.CreateShortURLCmd{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Err(shorturls.ErrShortURLBadRequest.Errorf("bad request data: %w", err)) @@ -37,7 +37,7 @@ func (hs *HTTPServer) createShortURL(c *models.ReqContext) response.Response { return response.JSON(http.StatusOK, dto) } -func (hs *HTTPServer) redirectFromShortURL(c *models.ReqContext) { +func (hs *HTTPServer) redirectFromShortURL(c *contextmodel.ReqContext) { shortURLUID := web.Params(c.Req)[":uid"] if !util.IsValidShortUID(shortURLUID) { diff --git a/pkg/api/short_url_test.go b/pkg/api/short_url_test.go index 4a25feec1a6..e84efbc9ce3 100644 --- a/pkg/api/short_url_test.go +++ b/pkg/api/short_url_test.go @@ -6,15 +6,16 @@ import ( "fmt" "testing" + "github.com/stretchr/testify/require" + "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/infra/log" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/shorturls" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/require" ) func TestShortURLAPIEndpoint(t *testing.T) { @@ -61,7 +62,7 @@ func createShortURLScenario(t *testing.T, desc string, url string, routePattern } sc := setupScenarioContext(t, url) - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(cmd) c.Req.Header.Add("Content-Type", "application/json") sc.context = c diff --git a/pkg/api/signup.go b/pkg/api/signup.go index 221749cb690..b14f74b4920 100644 --- a/pkg/api/signup.go +++ b/pkg/api/signup.go @@ -10,7 +10,7 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/events" "github.com/grafana/grafana/pkg/infra/metrics" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" tempuser "github.com/grafana/grafana/pkg/services/temp_user" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" @@ -19,7 +19,7 @@ import ( ) // GET /api/user/signup/options -func GetSignUpOptions(c *models.ReqContext) response.Response { +func GetSignUpOptions(c *contextmodel.ReqContext) response.Response { return response.JSON(http.StatusOK, util.DynMap{ "verifyEmailEnabled": setting.VerifyEmailEnabled, "autoAssignOrg": setting.AutoAssignOrg, @@ -27,7 +27,7 @@ func GetSignUpOptions(c *models.ReqContext) response.Response { } // POST /api/user/signup -func (hs *HTTPServer) SignUp(c *models.ReqContext) response.Response { +func (hs *HTTPServer) SignUp(c *contextmodel.ReqContext) response.Response { form := dtos.SignUpForm{} var err error if err = web.Bind(c.Req, &form); err != nil { @@ -75,7 +75,7 @@ func (hs *HTTPServer) SignUp(c *models.ReqContext) response.Response { return response.JSON(http.StatusOK, util.DynMap{"status": "SignUpCreated"}) } -func (hs *HTTPServer) SignUpStep2(c *models.ReqContext) response.Response { +func (hs *HTTPServer) SignUpStep2(c *contextmodel.ReqContext) response.Response { form := dtos.SignUpStep2Form{} if err := web.Bind(c.Req, &form); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) diff --git a/pkg/api/swagger.go b/pkg/api/swagger.go index 7c96b576b9f..667871fbe1b 100644 --- a/pkg/api/swagger.go +++ b/pkg/api/swagger.go @@ -3,9 +3,9 @@ package api import ( "net/http" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" ) -func swaggerUI(c *models.ReqContext) { +func swaggerUI(c *contextmodel.ReqContext) { c.HTML(http.StatusOK, "swagger", nil) } diff --git a/pkg/api/team.go b/pkg/api/team.go index d845b4f6ab1..5d5d2b6ed22 100644 --- a/pkg/api/team.go +++ b/pkg/api/team.go @@ -7,7 +7,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/team" @@ -25,7 +25,7 @@ import ( // 403: forbiddenError // 409: conflictError // 500: internalServerError -func (hs *HTTPServer) CreateTeam(c *models.ReqContext) response.Response { +func (hs *HTTPServer) CreateTeam(c *contextmodel.ReqContext) response.Response { cmd := team.CreateTeamCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -78,7 +78,7 @@ func (hs *HTTPServer) CreateTeam(c *models.ReqContext) response.Response { // 404: notFoundError // 409: conflictError // 500: internalServerError -func (hs *HTTPServer) UpdateTeam(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UpdateTeam(c *contextmodel.ReqContext) response.Response { cmd := team.UpdateTeamCommand{} var err error if err := web.Bind(c.Req, &cmd); err != nil { @@ -116,7 +116,7 @@ func (hs *HTTPServer) UpdateTeam(c *models.ReqContext) response.Response { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) DeleteTeamByID(c *models.ReqContext) response.Response { +func (hs *HTTPServer) DeleteTeamByID(c *contextmodel.ReqContext) response.Response { orgID := c.OrgID teamID, err := strconv.ParseInt(web.Params(c.Req)[":teamId"], 10, 64) if err != nil { @@ -148,7 +148,7 @@ func (hs *HTTPServer) DeleteTeamByID(c *models.ReqContext) response.Response { // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) SearchTeams(c *models.ReqContext) response.Response { +func (hs *HTTPServer) SearchTeams(c *contextmodel.ReqContext) response.Response { perPage := c.QueryInt("perpage") if perPage <= 0 { perPage = 1000 @@ -202,7 +202,7 @@ func (hs *HTTPServer) SearchTeams(c *models.ReqContext) response.Response { // UserFilter returns the user ID used in a filter when querying a team // 1. If the user is a viewer or editor, this will return the user's ID. // 2. If the user is an admin, this will return models.FilterIgnoreUser (0) -func userFilter(c *models.ReqContext) int64 { +func userFilter(c *contextmodel.ReqContext) int64 { userIdFilter := c.SignedInUser.UserID if c.OrgRole == org.RoleAdmin { userIdFilter = team.FilterIgnoreUser @@ -220,7 +220,7 @@ func userFilter(c *models.ReqContext) int64 { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetTeamByID(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetTeamByID(c *contextmodel.ReqContext) response.Response { teamId, err := strconv.ParseInt(web.Params(c.Req)[":teamId"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "teamId is invalid", err) @@ -264,7 +264,7 @@ func (hs *HTTPServer) GetTeamByID(c *models.ReqContext) response.Response { // 200: getPreferencesResponse // 401: unauthorisedError // 500: internalServerError -func (hs *HTTPServer) GetTeamPreferences(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetTeamPreferences(c *contextmodel.ReqContext) response.Response { teamId, err := strconv.ParseInt(web.Params(c.Req)[":teamId"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "teamId is invalid", err) @@ -290,7 +290,7 @@ func (hs *HTTPServer) GetTeamPreferences(c *models.ReqContext) response.Response // 400: badRequestError // 401: unauthorisedError // 500: internalServerError -func (hs *HTTPServer) UpdateTeamPreferences(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UpdateTeamPreferences(c *contextmodel.ReqContext) response.Response { dtoCmd := dtos.UpdatePrefsCmd{} if err := web.Bind(c.Req, &dtoCmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) diff --git a/pkg/api/team_members.go b/pkg/api/team_members.go index 630d99b8d85..3da7f5eda3b 100644 --- a/pkg/api/team_members.go +++ b/pkg/api/team_members.go @@ -9,8 +9,8 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/team" @@ -28,7 +28,7 @@ import ( // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetTeamMembers(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetTeamMembers(c *contextmodel.ReqContext) response.Response { teamId, err := strconv.ParseInt(web.Params(c.Req)[":teamId"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "teamId is invalid", err) @@ -79,7 +79,7 @@ func (hs *HTTPServer) GetTeamMembers(c *models.ReqContext) response.Response { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) AddTeamMember(c *models.ReqContext) response.Response { +func (hs *HTTPServer) AddTeamMember(c *contextmodel.ReqContext) response.Response { cmd := team.AddTeamMemberCommand{} var err error if err := web.Bind(c.Req, &cmd); err != nil { @@ -125,7 +125,7 @@ func (hs *HTTPServer) AddTeamMember(c *models.ReqContext) response.Response { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) UpdateTeamMember(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UpdateTeamMember(c *contextmodel.ReqContext) response.Response { cmd := team.UpdateTeamMemberCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -181,7 +181,7 @@ func getPermissionName(permission dashboards.PermissionType) string { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) RemoveTeamMember(c *models.ReqContext) response.Response { +func (hs *HTTPServer) RemoveTeamMember(c *contextmodel.ReqContext) response.Response { orgId := c.OrgID teamId, err := strconv.ParseInt(web.Params(c.Req)[":teamId"], 10, 64) if err != nil { diff --git a/pkg/api/team_test.go b/pkg/api/team_test.go index 3577ce501be..1423a2ea7c4 100644 --- a/pkg/api/team_test.go +++ b/pkg/api/team_test.go @@ -14,10 +14,10 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/db/dbtest" "github.com/grafana/grafana/pkg/infra/log/logtest" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/org" pref "github.com/grafana/grafana/pkg/services/preference" "github.com/grafana/grafana/pkg/services/preference/preftest" @@ -124,7 +124,7 @@ func TestTeamAPIEndpoint(t *testing.T) { t.Run("with no real signed in user", func(t *testing.T) { logger := &logtest.Fake{} - c := &models.ReqContext{ + c := &contextmodel.ReqContext{ Context: &web.Context{Req: req}, SignedInUser: &user.SignedInUser{}, Logger: logger, @@ -141,7 +141,7 @@ func TestTeamAPIEndpoint(t *testing.T) { t.Run("with real signed in user", func(t *testing.T) { logger := &logtest.Fake{} - c := &models.ReqContext{ + c := &contextmodel.ReqContext{ Context: &web.Context{Req: req}, SignedInUser: &user.SignedInUser{UserID: 42}, Logger: logger, diff --git a/pkg/api/user.go b/pkg/api/user.go index 91345a48cfa..997b86881fa 100644 --- a/pkg/api/user.go +++ b/pkg/api/user.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/team" @@ -29,7 +30,7 @@ import ( // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetSignedInUser(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetSignedInUser(c *contextmodel.ReqContext) response.Response { return hs.getUserUserProfile(c, c.UserID) } @@ -43,7 +44,7 @@ func (hs *HTTPServer) GetSignedInUser(c *models.ReqContext) response.Response { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetUserByID(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetUserByID(c *contextmodel.ReqContext) response.Response { id, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "id is invalid", err) @@ -51,7 +52,7 @@ func (hs *HTTPServer) GetUserByID(c *models.ReqContext) response.Response { return hs.getUserUserProfile(c, id) } -func (hs *HTTPServer) getUserUserProfile(c *models.ReqContext, userID int64) response.Response { +func (hs *HTTPServer) getUserUserProfile(c *contextmodel.ReqContext, userID int64) response.Response { query := user.GetUserProfileQuery{UserID: userID} userProfile, err := hs.userService.GetProfile(c.Req.Context(), &query) @@ -62,7 +63,7 @@ func (hs *HTTPServer) getUserUserProfile(c *models.ReqContext, userID int64) res return response.Error(500, "Failed to get user", err) } - getAuthQuery := models.GetAuthInfoQuery{UserId: userID} + getAuthQuery := login.GetAuthInfoQuery{UserId: userID} userProfile.AuthLabels = []string{} if err := hs.authInfoService.GetAuthInfo(c.Req.Context(), &getAuthQuery); err == nil { authLabel := login.GetAuthProviderLabel(getAuthQuery.Result.AuthModule) @@ -86,7 +87,7 @@ func (hs *HTTPServer) getUserUserProfile(c *models.ReqContext, userID int64) res // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetUserByLoginOrEmail(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetUserByLoginOrEmail(c *contextmodel.ReqContext) response.Response { query := user.GetUserByLoginQuery{LoginOrEmail: c.Query("loginOrEmail")} usr, err := hs.userService.GetByLogin(c.Req.Context(), &query) if err != nil { @@ -118,7 +119,7 @@ func (hs *HTTPServer) GetUserByLoginOrEmail(c *models.ReqContext) response.Respo // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) UpdateSignedInUser(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UpdateSignedInUser(c *contextmodel.ReqContext) response.Response { cmd := user.UpdateUserCommand{} var err error if err = web.Bind(c.Req, &cmd); err != nil { @@ -152,7 +153,7 @@ func (hs *HTTPServer) UpdateSignedInUser(c *models.ReqContext) response.Response // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) UpdateUser(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UpdateUser(c *contextmodel.ReqContext) response.Response { cmd := user.UpdateUserCommand{} var err error if err = web.Bind(c.Req, &cmd); err != nil { @@ -171,7 +172,7 @@ func (hs *HTTPServer) UpdateUser(c *models.ReqContext) response.Response { } // POST /api/users/:id/using/:orgId -func (hs *HTTPServer) UpdateUserActiveOrg(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UpdateUserActiveOrg(c *contextmodel.ReqContext) response.Response { userID, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "id is invalid", err) @@ -223,7 +224,7 @@ func (hs *HTTPServer) handleUpdateUser(ctx context.Context, cmd user.UpdateUserC } func (hs *HTTPServer) isExternalUser(ctx context.Context, userID int64) (bool, error) { - getAuthQuery := models.GetAuthInfoQuery{UserId: userID} + getAuthQuery := login.GetAuthInfoQuery{UserId: userID} var err error if err = hs.authInfoService.GetAuthInfo(ctx, &getAuthQuery); err == nil { return true, nil @@ -250,7 +251,7 @@ func (hs *HTTPServer) isExternalUser(ctx context.Context, userID int64) (bool, e // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) GetSignedInUserOrgList(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetSignedInUserOrgList(c *contextmodel.ReqContext) response.Response { return hs.getUserOrgList(c.Req.Context(), c.UserID) } @@ -265,7 +266,7 @@ func (hs *HTTPServer) GetSignedInUserOrgList(c *models.ReqContext) response.Resp // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) GetSignedInUserTeamList(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetSignedInUserTeamList(c *contextmodel.ReqContext) response.Response { return hs.getUserTeamList(c, c.OrgID, c.UserID) } @@ -281,7 +282,7 @@ func (hs *HTTPServer) GetSignedInUserTeamList(c *models.ReqContext) response.Res // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetUserTeams(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetUserTeams(c *contextmodel.ReqContext) response.Response { id, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "id is invalid", err) @@ -289,7 +290,7 @@ func (hs *HTTPServer) GetUserTeams(c *models.ReqContext) response.Response { return hs.getUserTeamList(c, c.OrgID, id) } -func (hs *HTTPServer) getUserTeamList(c *models.ReqContext, orgID int64, userID int64) response.Response { +func (hs *HTTPServer) getUserTeamList(c *contextmodel.ReqContext, orgID int64, userID int64) response.Response { query := team.GetTeamsByUserQuery{OrgID: orgID, UserID: userID, SignedInUser: c.SignedInUser} queryResult, err := hs.teamService.GetTeamsByUser(c.Req.Context(), &query) @@ -315,7 +316,7 @@ func (hs *HTTPServer) getUserTeamList(c *models.ReqContext, orgID int64, userID // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) GetUserOrgList(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetUserOrgList(c *contextmodel.ReqContext) response.Response { id, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "id is invalid", err) @@ -365,7 +366,7 @@ func (hs *HTTPServer) validateUsingOrg(ctx context.Context, userID int64, orgID // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) UserSetUsingOrg(c *models.ReqContext) response.Response { +func (hs *HTTPServer) UserSetUsingOrg(c *contextmodel.ReqContext) response.Response { orgID, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "id is invalid", err) @@ -385,7 +386,7 @@ func (hs *HTTPServer) UserSetUsingOrg(c *models.ReqContext) response.Response { } // GET /profile/switch-org/:id -func (hs *HTTPServer) ChangeActiveOrgAndRedirectToHome(c *models.ReqContext) { +func (hs *HTTPServer) ChangeActiveOrgAndRedirectToHome(c *contextmodel.ReqContext) { orgID, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64) if err != nil { c.JsonApiErr(http.StatusBadRequest, "id is invalid", err) @@ -420,7 +421,7 @@ func (hs *HTTPServer) ChangeActiveOrgAndRedirectToHome(c *models.ReqContext) { // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) ChangeUserPassword(c *models.ReqContext) response.Response { +func (hs *HTTPServer) ChangeUserPassword(c *contextmodel.ReqContext) response.Response { cmd := user.ChangeUserPasswordCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -433,7 +434,7 @@ func (hs *HTTPServer) ChangeUserPassword(c *models.ReqContext) response.Response return response.Error(500, "Could not read user from database", err) } - getAuthQuery := models.GetAuthInfoQuery{UserId: user.ID} + getAuthQuery := login.GetAuthInfoQuery{UserId: user.ID} if err := hs.authInfoService.GetAuthInfo(c.Req.Context(), &getAuthQuery); err == nil { authModule := getAuthQuery.Result.AuthModule if authModule == login.LDAPAuthModule || authModule == login.AuthProxyAuthModule { @@ -468,7 +469,7 @@ func (hs *HTTPServer) ChangeUserPassword(c *models.ReqContext) response.Response } // redirectToChangePassword handles GET /.well-known/change-password. -func redirectToChangePassword(c *models.ReqContext) { +func redirectToChangePassword(c *contextmodel.ReqContext) { c.Redirect("/profile/password", 302) } @@ -481,7 +482,7 @@ func redirectToChangePassword(c *models.ReqContext) { // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) SetHelpFlag(c *models.ReqContext) response.Response { +func (hs *HTTPServer) SetHelpFlag(c *contextmodel.ReqContext) response.Response { flag, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "id is invalid", err) @@ -511,7 +512,7 @@ func (hs *HTTPServer) SetHelpFlag(c *models.ReqContext) response.Response { // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) ClearHelpFlags(c *models.ReqContext) response.Response { +func (hs *HTTPServer) ClearHelpFlags(c *contextmodel.ReqContext) response.Response { cmd := user.SetUserHelpFlagCommand{ UserID: c.UserID, HelpFlags1: user.HelpFlags1(0), diff --git a/pkg/api/user_test.go b/pkg/api/user_test.go index 2bc4c156d0d..50be2a76ed1 100644 --- a/pkg/api/user_test.go +++ b/pkg/api/user_test.go @@ -19,8 +19,9 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/db/dbtest" "github.com/grafana/grafana/pkg/infra/usagestats" - "github.com/grafana/grafana/pkg/models" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" + "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/login/authinfoservice" authinfostore "github.com/grafana/grafana/pkg/services/login/authinfoservice/database" "github.com/grafana/grafana/pkg/services/login/logintest" @@ -89,9 +90,9 @@ func TestUserAPIEndpoint_userLoggedIn(t *testing.T) { } idToken := "testidtoken" token = token.WithExtra(map[string]interface{}{"id_token": idToken}) - login := "loginuser" - query := &models.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test", UserLookupParams: models.UserLookupParams{Login: &login}} - cmd := &models.UpdateAuthInfoCommand{ + userlogin := "loginuser" + query := &login.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test", UserLookupParams: login.UserLookupParams{Login: &userlogin}} + cmd := &login.UpdateAuthInfoCommand{ UserId: usr.ID, AuthId: query.AuthId, AuthModule: query.AuthModule, @@ -233,7 +234,7 @@ func TestHTTPServer_UpdateUser(t *testing.T) { routePattern: "/api/users/:id", cmd: updateUserCommand, fn: func(sc *scenarioContext) { - sc.authInfoService.ExpectedUserAuth = &models.UserAuth{} + sc.authInfoService.ExpectedUserAuth = &login.UserAuth{} sc.fakeReqWithParams("PUT", sc.url, map[string]string{"id": "1"}).exec() assert.Equal(t, 403, sc.resp.Code) }, @@ -255,7 +256,7 @@ func updateUserScenario(t *testing.T, ctx updateUserContext, hs *HTTPServer) { sc.authInfoService = &logintest.AuthInfoServiceFake{} hs.authInfoService = sc.authInfoService - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(ctx.cmd) c.Req.Header.Add("Content-Type", "application/json") sc.context = c @@ -294,7 +295,7 @@ func TestHTTPServer_UpdateSignedInUser(t *testing.T) { routePattern: "/api/users/", cmd: updateUserCommand, fn: func(sc *scenarioContext) { - sc.authInfoService.ExpectedUserAuth = &models.UserAuth{} + sc.authInfoService.ExpectedUserAuth = &login.UserAuth{} sc.fakeReqWithParams("PUT", sc.url, map[string]string{"id": "1"}).exec() assert.Equal(t, 403, sc.resp.Code) }, @@ -308,7 +309,7 @@ func updateSignedInUserScenario(t *testing.T, ctx updateUserContext, hs *HTTPSer sc.authInfoService = &logintest.AuthInfoServiceFake{} hs.authInfoService = sc.authInfoService - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(ctx.cmd) c.Req.Header.Add("Content-Type", "application/json") sc.context = c diff --git a/pkg/api/user_token.go b/pkg/api/user_token.go index 3e12fca2d2e..8a55c1e3385 100644 --- a/pkg/api/user_token.go +++ b/pkg/api/user_token.go @@ -6,14 +6,15 @@ import ( "net/http" "time" + "github.com/ua-parser/uap-go/uaparser" + "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/auth" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/web" - "github.com/ua-parser/uap-go/uaparser" ) // swagger:route GET /user/auth-tokens signed_in_user getUserAuthTokens @@ -27,7 +28,7 @@ import ( // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) GetUserAuthTokens(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetUserAuthTokens(c *contextmodel.ReqContext) response.Response { return hs.getUserAuthTokensInternal(c, c.UserID) } @@ -43,7 +44,7 @@ func (hs *HTTPServer) GetUserAuthTokens(c *models.ReqContext) response.Response // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) RevokeUserAuthToken(c *models.ReqContext) response.Response { +func (hs *HTTPServer) RevokeUserAuthToken(c *contextmodel.ReqContext) response.Response { cmd := auth.RevokeAuthTokenCmd{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -72,7 +73,7 @@ func (hs *HTTPServer) logoutUserFromAllDevicesInternal(ctx context.Context, user }) } -func (hs *HTTPServer) getUserAuthTokensInternal(c *models.ReqContext, userID int64) response.Response { +func (hs *HTTPServer) getUserAuthTokensInternal(c *contextmodel.ReqContext, userID int64) response.Response { userQuery := user.GetUserByIDQuery{ID: userID} _, err := hs.userService.GetByID(c.Req.Context(), &userQuery) @@ -144,7 +145,7 @@ func (hs *HTTPServer) getUserAuthTokensInternal(c *models.ReqContext, userID int return response.JSON(http.StatusOK, result) } -func (hs *HTTPServer) revokeUserAuthTokenInternal(c *models.ReqContext, userID int64, cmd auth.RevokeAuthTokenCmd) response.Response { +func (hs *HTTPServer) revokeUserAuthTokenInternal(c *contextmodel.ReqContext, userID int64, cmd auth.RevokeAuthTokenCmd) response.Response { userQuery := user.GetUserByIDQuery{ID: userID} _, err := hs.userService.GetByID(c.Req.Context(), &userQuery) if err != nil { diff --git a/pkg/api/user_token_test.go b/pkg/api/user_token_test.go index 093a27011b9..a6d54f951fc 100644 --- a/pkg/api/user_token_test.go +++ b/pkg/api/user_token_test.go @@ -10,9 +10,9 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/auth/authtest" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/usertest" @@ -158,7 +158,7 @@ func revokeUserAuthTokenScenario(t *testing.T, desc string, url string, routePat sc := setupScenarioContext(t, url) sc.userAuthTokenService = fakeAuthTokenService - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(cmd) sc.context = c sc.context.UserID = userId @@ -185,7 +185,7 @@ func getUserAuthTokensScenario(t *testing.T, desc string, url string, routePatte sc := setupScenarioContext(t, url) sc.userAuthTokenService = fakeAuthTokenService - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { sc.context = c sc.context.UserID = userId sc.context.OrgID = testOrgID @@ -208,7 +208,7 @@ func logoutUserFromAllDevicesInternalScenario(t *testing.T, desc string, userId } sc := setupScenarioContext(t, "/") - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { sc.context = c sc.context.UserID = testUserID sc.context.OrgID = testOrgID @@ -235,7 +235,7 @@ func revokeUserAuthTokenInternalScenario(t *testing.T, desc string, cmd auth.Rev sc := setupScenarioContext(t, "/") sc.userAuthTokenService = fakeAuthTokenService - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { sc.context = c sc.context.UserID = testUserID sc.context.OrgID = testOrgID @@ -260,7 +260,7 @@ func getUserAuthTokensInternalScenario(t *testing.T, desc string, token *auth.Us sc := setupScenarioContext(t, "/") sc.userAuthTokenService = fakeAuthTokenService - sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { + sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { sc.context = c sc.context.UserID = testUserID sc.context.OrgID = testOrgID diff --git a/pkg/build/cmd/buildbackend.go b/pkg/build/cmd/buildbackend.go index 39f0d707fe4..40c4f90388a 100644 --- a/pkg/build/cmd/buildbackend.go +++ b/pkg/build/cmd/buildbackend.go @@ -4,12 +4,13 @@ import ( "fmt" "log" + "github.com/urfave/cli/v2" + "github.com/grafana/grafana/pkg/build/compilers" "github.com/grafana/grafana/pkg/build/config" "github.com/grafana/grafana/pkg/build/errutil" "github.com/grafana/grafana/pkg/build/grafana" "github.com/grafana/grafana/pkg/build/syncutil" - "github.com/urfave/cli/v2" ) func BuildBackend(ctx *cli.Context) error { diff --git a/pkg/build/cmd/builddocker.go b/pkg/build/cmd/builddocker.go index ac1858a65ba..0dffd5dd2c8 100644 --- a/pkg/build/cmd/builddocker.go +++ b/pkg/build/cmd/builddocker.go @@ -3,10 +3,11 @@ package main import ( "log" + "github.com/urfave/cli/v2" + "github.com/grafana/grafana/pkg/build/config" "github.com/grafana/grafana/pkg/build/docker" "github.com/grafana/grafana/pkg/build/gcloud" - "github.com/urfave/cli/v2" ) func BuildDocker(c *cli.Context) error { diff --git a/pkg/build/cmd/buildfrontend.go b/pkg/build/cmd/buildfrontend.go index d7825f419cd..1f64ab0b0e8 100644 --- a/pkg/build/cmd/buildfrontend.go +++ b/pkg/build/cmd/buildfrontend.go @@ -3,11 +3,12 @@ package main import ( "log" + "github.com/urfave/cli/v2" + "github.com/grafana/grafana/pkg/build/config" "github.com/grafana/grafana/pkg/build/errutil" "github.com/grafana/grafana/pkg/build/frontend" "github.com/grafana/grafana/pkg/build/syncutil" - "github.com/urfave/cli/v2" ) func BuildFrontend(c *cli.Context) error { diff --git a/pkg/build/cmd/buildfrontendpackages.go b/pkg/build/cmd/buildfrontendpackages.go index 786ebccaff7..b3c07039818 100644 --- a/pkg/build/cmd/buildfrontendpackages.go +++ b/pkg/build/cmd/buildfrontendpackages.go @@ -4,10 +4,11 @@ import ( "log" "strings" + "github.com/urfave/cli/v2" + "github.com/grafana/grafana/pkg/build/errutil" "github.com/grafana/grafana/pkg/build/frontend" "github.com/grafana/grafana/pkg/build/syncutil" - "github.com/urfave/cli/v2" ) func BuildFrontendPackages(c *cli.Context) error { diff --git a/pkg/build/cmd/buildinternalplugins.go b/pkg/build/cmd/buildinternalplugins.go index fad80b818d5..15a1397158d 100644 --- a/pkg/build/cmd/buildinternalplugins.go +++ b/pkg/build/cmd/buildinternalplugins.go @@ -4,11 +4,12 @@ import ( "context" "log" + "github.com/urfave/cli/v2" + "github.com/grafana/grafana/pkg/build/config" "github.com/grafana/grafana/pkg/build/errutil" "github.com/grafana/grafana/pkg/build/plugins" "github.com/grafana/grafana/pkg/build/syncutil" - "github.com/urfave/cli/v2" ) func BuildInternalPlugins(c *cli.Context) error { diff --git a/pkg/build/cmd/e2etests.go b/pkg/build/cmd/e2etests.go index f377b08f874..ec27fc9b28f 100644 --- a/pkg/build/cmd/e2etests.go +++ b/pkg/build/cmd/e2etests.go @@ -6,8 +6,9 @@ import ( "os" "os/exec" - "github.com/grafana/grafana/pkg/build/e2eutil" "github.com/urfave/cli/v2" + + "github.com/grafana/grafana/pkg/build/e2eutil" ) func EndToEndTests(c *cli.Context) error { diff --git a/pkg/build/cmd/enterprisecheck.go b/pkg/build/cmd/enterprisecheck.go index f2c36b0fc2e..3fb13f6c893 100644 --- a/pkg/build/cmd/enterprisecheck.go +++ b/pkg/build/cmd/enterprisecheck.go @@ -6,9 +6,10 @@ import ( "os" "strconv" + "github.com/urfave/cli/v2" + "github.com/grafana/grafana/pkg/build/env" "github.com/grafana/grafana/pkg/build/git" - "github.com/urfave/cli/v2" ) // checkOpts are options used to create a new GitHub check for the enterprise downstream test. diff --git a/pkg/build/cmd/exportversion.go b/pkg/build/cmd/exportversion.go index a3a54309c73..c4c4744ebef 100644 --- a/pkg/build/cmd/exportversion.go +++ b/pkg/build/cmd/exportversion.go @@ -4,8 +4,9 @@ import ( "os" "path/filepath" - "github.com/grafana/grafana/pkg/build/config" "github.com/urfave/cli/v2" + + "github.com/grafana/grafana/pkg/build/config" ) func ExportVersion(c *cli.Context) error { diff --git a/pkg/build/cmd/fetchimages.go b/pkg/build/cmd/fetchimages.go index bd1ec6a5102..b131cc9a73d 100644 --- a/pkg/build/cmd/fetchimages.go +++ b/pkg/build/cmd/fetchimages.go @@ -6,10 +6,11 @@ import ( "os/exec" "strings" + "github.com/urfave/cli/v2" + "github.com/grafana/grafana/pkg/build/config" "github.com/grafana/grafana/pkg/build/docker" "github.com/grafana/grafana/pkg/build/gcloud" - "github.com/urfave/cli/v2" ) const ( diff --git a/pkg/build/cmd/grafanacom.go b/pkg/build/cmd/grafanacom.go index 43f5bc9eb0f..b2559c2d96b 100644 --- a/pkg/build/cmd/grafanacom.go +++ b/pkg/build/cmd/grafanacom.go @@ -9,7 +9,6 @@ import ( "net/http" "net/url" "os" - "path" "path/filepath" "strings" diff --git a/pkg/build/cmd/main.go b/pkg/build/cmd/main.go index 69a326f5335..2613c50bff0 100644 --- a/pkg/build/cmd/main.go +++ b/pkg/build/cmd/main.go @@ -5,8 +5,9 @@ import ( "os" "strings" - "github.com/grafana/grafana/pkg/build/docker" "github.com/urfave/cli/v2" + + "github.com/grafana/grafana/pkg/build/docker" ) func main() { @@ -132,6 +133,12 @@ func main() { Usage: "Verify Drone configuration", Action: VerifyDrone, }, + { + Name: "verify-starlark", + Usage: "Verify Starlark configuration", + ArgsUsage: "", + Action: VerifyStarlark, + }, { Name: "export-version", Usage: "Exports version in dist/grafana.version", @@ -183,6 +190,59 @@ func main() { Name: "artifacts", Usage: "Handle Grafana artifacts", Subcommands: cli.Commands{ + { + Name: "publish", + Usage: "Publish Grafana artifacts", + Action: PublishArtifactsAction, + Flags: []cli.Flag{ + &editionFlag, + &cli.BoolFlag{ + Name: "security", + Usage: "Security release", + }, + &cli.StringFlag{ + Name: "security-dest-bucket", + Usage: "Google Cloud Storage bucket for security packages (or $SECURITY_DEST_BUCKET)", + }, + &cli.StringFlag{ + Name: "tag", + Usage: "Grafana version tag", + }, + &cli.StringFlag{ + Name: "src-bucket", + Value: "grafana-prerelease", + Usage: "Google Cloud Storage bucket", + }, + &cli.StringFlag{ + Name: "dest-bucket", + Value: "grafana-downloads", + Usage: "Google Cloud Storage bucket for published packages", + }, + &cli.StringFlag{ + Name: "enterprise2-dest-bucket", + Value: "grafana-downloads-enterprise2", + Usage: "Google Cloud Storage bucket for published packages", + }, + &cli.StringFlag{ + Name: "enterprise2-security-prefix", + Usage: "Bucket path prefix for enterprise2 security releases (or $ENTERPRISE2_SECURITY_PREFIX)", + }, + &cli.StringFlag{ + Name: "static-assets-bucket", + Value: "grafana-static-assets", + Usage: "Google Cloud Storage bucket for static assets", + }, + &cli.StringSliceFlag{ + Name: "static-asset-editions", + Usage: "All the editions of the static assets (or $STATIC_ASSET_EDITIONS)", + }, + &cli.StringFlag{ + Name: "storybook-bucket", + Value: "grafana-storybook", + Usage: "Google Cloud Storage bucket for storybooks", + }, + }, + }, { Name: "docker", Usage: "Handle Grafana Docker images", diff --git a/pkg/build/cmd/npm.go b/pkg/build/cmd/npm.go index 07c2fe1365f..13994fb4e19 100644 --- a/pkg/build/cmd/npm.go +++ b/pkg/build/cmd/npm.go @@ -6,8 +6,9 @@ import ( "os/exec" "strings" - "github.com/grafana/grafana/pkg/build/npm" "github.com/urfave/cli/v2" + + "github.com/grafana/grafana/pkg/build/npm" ) func NpmRetrieveAction(c *cli.Context) error { diff --git a/pkg/build/cmd/package.go b/pkg/build/cmd/package.go index b91f1be0179..729856f6834 100644 --- a/pkg/build/cmd/package.go +++ b/pkg/build/cmd/package.go @@ -5,11 +5,12 @@ import ( "log" "strings" + "github.com/urfave/cli/v2" + "github.com/grafana/grafana/pkg/build/config" "github.com/grafana/grafana/pkg/build/gpg" "github.com/grafana/grafana/pkg/build/packaging" "github.com/grafana/grafana/pkg/build/syncutil" - "github.com/urfave/cli/v2" ) func Package(c *cli.Context) error { diff --git a/pkg/build/cmd/publishartifacts.go b/pkg/build/cmd/publishartifacts.go new file mode 100644 index 00000000000..bd158d6d3ad --- /dev/null +++ b/pkg/build/cmd/publishartifacts.go @@ -0,0 +1,211 @@ +package main + +import ( + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/grafana/grafana/pkg/build/gcloud" + "github.com/grafana/grafana/pkg/build/versions" + "github.com/urfave/cli/v2" +) + +type publishConfig struct { + tag string + srcBucket string + destBucket string + enterprise2DestBucket string + enterprise2SecurityPrefix string + staticAssetsBucket string + staticAssetEditions []string + storybookBucket string + security bool +} + +// requireListWithEnvFallback first checks the CLI for a flag with the required +// name. If this is empty, it falls back to taking the environment variable. +// Sadly, we cannot use cli.Flag.EnvVars for this due to it potentially leaking +// environment variables as default values in usage-errors. +func requireListWithEnvFallback(cctx *cli.Context, name string, envName string) ([]string, error) { + result := cctx.StringSlice(name) + if len(result) == 0 { + for _, v := range strings.Split(os.Getenv(envName), ",") { + value := strings.TrimSpace(v) + if value != "" { + result = append(result, value) + } + } + } + if len(result) == 0 { + return nil, cli.Exit(fmt.Sprintf("Required flag (%s) or environment variable (%s) not set", name, envName), 1) + } + return result, nil +} + +func requireStringWithEnvFallback(cctx *cli.Context, name string, envName string) (string, error) { + result := cctx.String(name) + if result == "" { + result = os.Getenv(envName) + } + if result == "" { + return "", cli.Exit(fmt.Sprintf("Required flag (%s) or environment variable (%s) not set", name, envName), 1) + } + return result, nil +} + +// Action implements the sub-command "publish-artifacts". +func PublishArtifactsAction(c *cli.Context) error { + if c.NArg() > 0 { + if err := cli.ShowSubcommandHelp(c); err != nil { + return cli.Exit(err.Error(), 1) + } + return cli.Exit("", 1) + } + + staticAssetEditions, err := requireListWithEnvFallback(c, "static-asset-editions", "STATIC_ASSET_EDITIONS") + if err != nil { + return err + } + securityDestBucket, err := requireStringWithEnvFallback(c, "security-dest-bucket", "SECURITY_DEST_BUCKET") + if err != nil { + return err + } + enterprise2SecurityPrefix, err := requireStringWithEnvFallback(c, "enterprise2-security-prefix", "ENTERPRISE2_SECURITY_PREFIX") + if err != nil { + return err + } + + if err := gcloud.ActivateServiceAccount(); err != nil { + return fmt.Errorf("error connecting to gcp, %q", err) + } + + cfg := publishConfig{ + srcBucket: c.String("src-bucket"), + destBucket: c.String("dest-bucket"), + enterprise2DestBucket: c.String("enterprise2-dest-bucket"), + enterprise2SecurityPrefix: enterprise2SecurityPrefix, + staticAssetsBucket: c.String("static-assets-bucket"), + staticAssetEditions: staticAssetEditions, + storybookBucket: c.String("storybook-bucket"), + security: c.Bool("security"), + tag: strings.TrimPrefix(c.String("tag"), "v"), + } + + if cfg.security { + cfg.destBucket = securityDestBucket + } + + err = copyStaticAssets(cfg) + if err != nil { + return err + } + err = copyStorybook(cfg) + if err != nil { + return err + } + err = copyDownloads(cfg) + if err != nil { + return err + } + err = copyEnterprise2Downloads(cfg) + if err != nil { + return err + } + return nil +} + +func copyStaticAssets(cfg publishConfig) error { + for _, edition := range cfg.staticAssetEditions { + log.Printf("Copying static assets for %s", edition) + srcURL := fmt.Sprintf("%s/artifacts/static-assets/%s/%s/*", cfg.srcBucket, edition, cfg.tag) + destURL := fmt.Sprintf("%s/%s/%s/", cfg.staticAssetsBucket, edition, cfg.tag) + err := gcsCopy("static assets", srcURL, destURL) + if err != nil { + return fmt.Errorf("error copying static assets, %q", err) + } + } + log.Printf("Successfully copied static assets!") + return nil +} + +func copyStorybook(cfg publishConfig) error { + if cfg.security { + log.Printf("skipping storybook copy - not needed for a security release") + return nil + } + log.Printf("Copying storybooks...") + srcURL := fmt.Sprintf("%s/artifacts/storybook/v%s/*", cfg.srcBucket, cfg.tag) + destURL := fmt.Sprintf("%s/%s", cfg.storybookBucket, cfg.tag) + err := gcsCopy("storybook", srcURL, destURL) + if err != nil { + return fmt.Errorf("error copying storybook. %q", err) + } + stableVersion, err := versions.GetLatestVersion(versions.LatestStableVersionURL) + if err != nil { + return err + } + isLatest, err := versions.IsGreaterThanOrEqual(cfg.tag, stableVersion) + if err != nil { + return err + } + if isLatest { + log.Printf("Copying storybooks to latest...") + srcURL := fmt.Sprintf("%s/artifacts/storybook/v%s/*", cfg.srcBucket, cfg.tag) + destURL := fmt.Sprintf("%s/latest", cfg.storybookBucket) + err := gcsCopy("storybook (latest)", srcURL, destURL) + if err != nil { + return fmt.Errorf("error copying storybook to latest. %q", err) + } + } + + log.Printf("Successfully copied storybook!") + return nil +} + +func copyDownloads(cfg publishConfig) error { + for _, edition := range []string{ + "oss", "enterprise", + } { + destURL := fmt.Sprintf("%s/%s/", cfg.destBucket, edition) + srcURL := fmt.Sprintf("%s/artifacts/downloads/v%s/%s/release/*", cfg.srcBucket, cfg.tag, edition) + if !cfg.security { + destURL = filepath.Join(destURL, "release") + } + log.Printf("Copying downloads for %s, from %s bucket to %s bucket", edition, srcURL, destURL) + err := gcsCopy("downloads", srcURL, destURL) + if err != nil { + return fmt.Errorf("error copying downloads, %q", err) + } + } + log.Printf("Successfully copied downloads.") + return nil +} + +func copyEnterprise2Downloads(cfg publishConfig) error { + var prefix string + if cfg.security { + prefix = cfg.enterprise2SecurityPrefix + } + srcURL := fmt.Sprintf("%s/artifacts/downloads-enterprise2/v%s/enterprise2/release/*", cfg.srcBucket, cfg.tag) + destURL := fmt.Sprintf("%s/enterprise2/%srelease", cfg.enterprise2DestBucket, prefix) + log.Printf("Copying downloads for enterprise2, from %s bucket to %s bucket", srcURL, destURL) + err := gcsCopy("enterprise2 downloads", srcURL, destURL) + if err != nil { + return fmt.Errorf("error copying ") + } + return nil +} + +func gcsCopy(desc, src, dest string) error { + args := strings.Split(fmt.Sprintf("-m cp -r gs://%s gs://%s", src, dest), " ") + // nolint:gosec + cmd := exec.Command("gsutil", args...) + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("failed to publish %s: %w\n%s", desc, err, out) + } + return nil +} diff --git a/pkg/build/cmd/publishaws.go b/pkg/build/cmd/publishaws.go index e499c6b27fa..5fc98fbbc27 100644 --- a/pkg/build/cmd/publishaws.go +++ b/pkg/build/cmd/publishaws.go @@ -17,8 +17,9 @@ import ( "github.com/aws/aws-sdk-go/service/marketplacecatalog" "github.com/docker/docker/api/types" "github.com/docker/docker/client" - "github.com/grafana/grafana/pkg/build/config" "github.com/urfave/cli/v2" + + "github.com/grafana/grafana/pkg/build/config" ) const ( diff --git a/pkg/build/cmd/publishgithub.go b/pkg/build/cmd/publishgithub.go index 49366cecd27..c59b1471ae4 100644 --- a/pkg/build/cmd/publishgithub.go +++ b/pkg/build/cmd/publishgithub.go @@ -9,9 +9,10 @@ import ( "strings" "github.com/google/go-github/github" - "github.com/grafana/grafana/pkg/build/config" "github.com/urfave/cli/v2" "golang.org/x/oauth2" + + "github.com/grafana/grafana/pkg/build/config" ) type githubRepositoryService interface { diff --git a/pkg/build/cmd/publishimages_enterprise2.go b/pkg/build/cmd/publishimages_enterprise2.go index b76b2688d6f..94cd4b57c7d 100644 --- a/pkg/build/cmd/publishimages_enterprise2.go +++ b/pkg/build/cmd/publishimages_enterprise2.go @@ -6,10 +6,11 @@ import ( "os" "os/exec" + "github.com/urfave/cli/v2" + "github.com/grafana/grafana/pkg/build/config" "github.com/grafana/grafana/pkg/build/docker" "github.com/grafana/grafana/pkg/build/gcloud" - "github.com/urfave/cli/v2" ) func Enterprise2(c *cli.Context) error { diff --git a/pkg/build/cmd/storestorybook.go b/pkg/build/cmd/storestorybook.go index 65adfa1fbb7..13970e6a32e 100644 --- a/pkg/build/cmd/storestorybook.go +++ b/pkg/build/cmd/storestorybook.go @@ -4,9 +4,10 @@ import ( "log" "path/filepath" + "github.com/urfave/cli/v2" + "github.com/grafana/grafana/pkg/build/config" "github.com/grafana/grafana/pkg/build/gcloud/storage" - "github.com/urfave/cli/v2" ) // StoreStorybook implements the sub-command "store-storybook". diff --git a/pkg/build/cmd/uploadcdn.go b/pkg/build/cmd/uploadcdn.go index 8b8ddb0cd81..e18e1f65e16 100644 --- a/pkg/build/cmd/uploadcdn.go +++ b/pkg/build/cmd/uploadcdn.go @@ -6,9 +6,10 @@ import ( "os" "path/filepath" + "github.com/urfave/cli/v2" + "github.com/grafana/grafana/pkg/build/config" "github.com/grafana/grafana/pkg/build/gcloud/storage" - "github.com/urfave/cli/v2" ) // UploadCDN implements the sub-command "upload-cdn". diff --git a/pkg/build/cmd/uploadpackages.go b/pkg/build/cmd/uploadpackages.go index 3270948ae66..136ae56bc42 100644 --- a/pkg/build/cmd/uploadpackages.go +++ b/pkg/build/cmd/uploadpackages.go @@ -9,11 +9,12 @@ import ( "path/filepath" "strings" + "github.com/urfave/cli/v2" + "github.com/grafana/grafana/pkg/build/config" "github.com/grafana/grafana/pkg/build/droneutil" "github.com/grafana/grafana/pkg/build/gcloud" "github.com/grafana/grafana/pkg/build/packaging" - "github.com/urfave/cli/v2" ) const releaseFolder = "release" diff --git a/pkg/build/cmd/uploadpackages_test.go b/pkg/build/cmd/uploadpackages_test.go index cdb7bdab945..af44a66d994 100644 --- a/pkg/build/cmd/uploadpackages_test.go +++ b/pkg/build/cmd/uploadpackages_test.go @@ -5,9 +5,10 @@ import ( "fmt" "testing" - "github.com/grafana/grafana/pkg/build/config" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/build/config" ) func Test_getVersionFolder(t *testing.T) { diff --git a/pkg/build/cmd/verifydrone.go b/pkg/build/cmd/verifydrone.go index 19da6fb5a35..4794efa4500 100644 --- a/pkg/build/cmd/verifydrone.go +++ b/pkg/build/cmd/verifydrone.go @@ -10,9 +10,7 @@ import ( "github.com/drone/drone-cli/drone/lint" "github.com/drone/drone-cli/drone/starlark" - "github.com/google/go-cmp/cmp" - cliv1 "github.com/urfave/cli" "github.com/urfave/cli/v2" "gopkg.in/yaml.v3" diff --git a/pkg/build/cmd/verifystarlark.go b/pkg/build/cmd/verifystarlark.go new file mode 100644 index 00000000000..ff33a77a3af --- /dev/null +++ b/pkg/build/cmd/verifystarlark.go @@ -0,0 +1,142 @@ +package main + +import ( + "context" + "errors" + "fmt" + "io/fs" + "os/exec" + "path/filepath" + "strings" + + "github.com/urfave/cli/v2" +) + +func mapSlice[I any, O any](a []I, f func(I) O) []O { + o := make([]O, len(a)) + for i, e := range a { + o[i] = f(e) + } + return o +} + +// VerifyStarlark is the CLI Action for verifying Starlark files in a workspace. +// It expects a single context argument which is the path to the workspace. +// The actual verification procedure can return multiple errors which are +// joined together to be one holistic error for the action. +func VerifyStarlark(c *cli.Context) error { + if c.NArg() != 1 { + var message string + if c.NArg() == 0 { + message = "ERROR: missing required argument " + } + if c.NArg() > 1 { + message = "ERROR: too many arguments" + } + + if err := cli.ShowSubcommandHelp(c); err != nil { + return err + } + + return cli.Exit(message, 1) + } + + workspace := c.Args().Get(0) + verificationErrs, executionErr := verifyStarlark(c.Context, workspace, buildifierLintCommand) + if executionErr != nil { + return executionErr + } + + if len(verificationErrs) == 0 { + return nil + } + + noun := "file" + if len(verificationErrs) > 1 { + noun += "s" + } + + return fmt.Errorf("verification failed for %d %s:\n%s", + len(verificationErrs), + noun, + strings.Join( + mapSlice(verificationErrs, func(e error) string { return e.Error() }), + "\n", + )) +} + +type commandFunc = func(path string) (command string, args []string) + +func buildifierLintCommand(path string) (string, []string) { + return "buildifier", []string{"-lint", "warn", "-mode", "check", path} +} + +// verifyStarlark walks all directories starting at provided workspace path and +// verifies any Starlark files it finds. +// Starlark files are assumed to end with the .star extension. +// The verification relies on linting frovided by the 'buildifier' binary which +// must be in the PATH. +// A slice of verification errors are returned, one for each file that failed verification. +// If any execution of the `buildifier` command fails, this is returned separately. +// commandFn is executed on every Starlark file to determine the command and arguments to be executed. +// The caller is trusted and it is the callers responsibility to ensure that the resulting command is safe to execute. +func verifyStarlark(ctx context.Context, workspace string, commandFn commandFunc) ([]error, error) { + var verificationErrs []error + + // All errors from filepath.WalkDir are filtered by the fs.WalkDirFunc. + // Lstat or ReadDir errors are reported as verificationErrors. + // If any execution of the `buildifier` command fails or if the context is cancelled, + // it is reported as an error and any verification of subsequent files is skipped. + err := filepath.WalkDir(workspace, func(path string, d fs.DirEntry, err error) error { + // Skip verification of the file or files within the directory if there is an error + // returned by Lstat or ReadDir. + if err != nil { + verificationErrs = append(verificationErrs, err) + return nil + } + + if d.IsDir() { + return nil + } + + if filepath.Ext(path) == ".star" { + command, args := commandFn(path) + // The caller is trusted. + //nolint:gosec + cmd := exec.CommandContext(ctx, command, args...) + cmd.Dir = workspace + + _, err = cmd.Output() + if err == nil { // No error, early return. + return nil + } + + // The error returned from cmd.Output() is never wrapped. + //nolint:errorlint + if err, ok := err.(*exec.ExitError); ok { + switch err.ExitCode() { + // Case comments are informed by the output of `buildifier --help` + case 1: // syntax errors in input + verificationErrs = append(verificationErrs, errors.New(string(err.Stderr))) + return nil + case 2: // usage errors: invoked incorrectly + return fmt.Errorf("command %q: %s", cmd, err.Stderr) + case 3: // unexpected runtime errors: file I/O problems or internal bugs + return fmt.Errorf("command %q: %s", cmd, err.Stderr) + case 4: // check mode failed (reformat is needed) + verificationErrs = append(verificationErrs, errors.New(string(err.Stderr))) + return nil + default: + return fmt.Errorf("command %q: %s", cmd, err.Stderr) + } + } + + // Error was not an exit error from the command. + return fmt.Errorf("command %q: %v", cmd, err) + } + + return nil + }) + + return verificationErrs, err +} diff --git a/pkg/build/cmd/verifystarlark_test.go b/pkg/build/cmd/verifystarlark_test.go new file mode 100644 index 00000000000..8fe61dc2e8c --- /dev/null +++ b/pkg/build/cmd/verifystarlark_test.go @@ -0,0 +1,135 @@ +package main + +import ( + "context" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestVerifyStarlark(t *testing.T) { + t.Run("execution errors", func(t *testing.T) { + t.Run("invalid usage", func(t *testing.T) { + ctx := context.Background() + workspace := t.TempDir() + err := os.WriteFile(filepath.Join(workspace, "ignored.star"), []byte{}, os.ModePerm) + if err != nil { + t.Fatalf(err.Error()) + } + + _, executionErr := verifyStarlark(ctx, workspace, func(string) (string, []string) { return "buildifier", []string{"--invalid"} }) + if executionErr == nil { + t.Fatalf("Expected execution error but got none") + } + }) + + t.Run("context cancellation", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + workspace := t.TempDir() + err := os.WriteFile(filepath.Join(workspace, "ignored.star"), []byte{}, os.ModePerm) + if err != nil { + t.Fatalf(err.Error()) + } + err = os.WriteFile(filepath.Join(workspace, "other-ignored.star"), []byte{}, os.ModePerm) + if err != nil { + t.Fatalf(err.Error()) + } + cancel() + + _, executionErr := verifyStarlark(ctx, workspace, buildifierLintCommand) + if executionErr == nil { + t.Fatalf("Expected execution error but got none") + } + }) + }) + + t.Run("verification errors", func(t *testing.T) { + t.Run("a single file with lint", func(t *testing.T) { + ctx := context.Background() + workspace := t.TempDir() + + invalidContent := []byte(`load("scripts/drone/other.star", "function") + +function()`) + err := os.WriteFile(filepath.Join(workspace, "has-lint.star"), invalidContent, os.ModePerm) + if err != nil { + t.Fatalf(err.Error()) + } + + verificationErrs, executionErr := verifyStarlark(ctx, workspace, buildifierLintCommand) + if executionErr != nil { + t.Fatalf("Unexpected execution error: %v", executionErr) + } + if len(verificationErrs) == 0 { + t.Fatalf(`"has-lint.star" requires linting but the verifyStarlark function provided no linting error`) + } + if len(verificationErrs) > 1 { + t.Fatalf(`verifyStarlark returned multiple errors for the "has-lint.star" file but only one was expected: %v`, verificationErrs) + } + if !strings.Contains(verificationErrs[0].Error(), "has-lint.star:1: module-docstring: The file has no module docstring.") { + t.Fatalf(`"has-lint.star" is missing a module docstring but the verifyStarlark function linting error did not mention this, instead we got: %v`, verificationErrs[0]) + } + }) + + t.Run("no files with lint", func(t *testing.T) { + ctx := context.Background() + workspace := t.TempDir() + + content := []byte(`""" +This module does nothing. +""" + +load("scripts/drone/other.star", "function") + +function() +`) + require.NoError(t, os.WriteFile(filepath.Join(workspace, "no-lint.star"), content, os.ModePerm)) + + verificationErrs, executionErr := verifyStarlark(ctx, workspace, buildifierLintCommand) + if executionErr != nil { + t.Fatalf("Unexpected execution error: %v", executionErr) + } + if len(verificationErrs) != 0 { + t.Log(`"no-lint.star" has no lint but the verifyStarlark function provided at least one error`) + for _, err := range verificationErrs { + t.Log(err) + } + t.FailNow() + } + }) + + t.Run("multiple files with lint", func(t *testing.T) { + ctx := context.Background() + workspace := t.TempDir() + + invalidContent := []byte(`load("scripts/drone/other.star", "function") + +function()`) + require.NoError(t, os.WriteFile(filepath.Join(workspace, "has-lint.star"), invalidContent, os.ModePerm)) + require.NoError(t, os.WriteFile(filepath.Join(workspace, "has-lint2.star"), invalidContent, os.ModePerm)) + + verificationErrs, executionErr := verifyStarlark(ctx, workspace, buildifierLintCommand) + if executionErr != nil { + t.Fatalf("Unexpected execution error: %v", executionErr) + } + if len(verificationErrs) == 0 { + t.Fatalf(`Two files require linting but the verifyStarlark function provided no linting error`) + } + if len(verificationErrs) == 1 { + t.Fatalf(`Two files require linting but the verifyStarlark function provided only one linting error: %v`, verificationErrs[0]) + } + if len(verificationErrs) > 2 { + t.Fatalf(`verifyStarlark returned more errors than expected: %v`, verificationErrs) + } + if !strings.Contains(verificationErrs[0].Error(), "has-lint.star:1: module-docstring: The file has no module docstring.") { + t.Errorf(`"has-lint.star" is missing a module docstring but the verifyStarlark function linting error did not mention this, instead we got: %v`, verificationErrs[0]) + } + if !strings.Contains(verificationErrs[1].Error(), "has-lint2.star:1: module-docstring: The file has no module docstring.") { + t.Fatalf(`"has-lint2.star" is missing a module docstring but the verifyStarlark function linting error did not mention this, instead we got: %v`, verificationErrs[0]) + } + }) + }) +} diff --git a/pkg/build/config/genmetadata.go b/pkg/build/config/genmetadata.go index c201d77dd39..74ffde8f201 100644 --- a/pkg/build/config/genmetadata.go +++ b/pkg/build/config/genmetadata.go @@ -5,8 +5,9 @@ import ( "os" "strings" - "github.com/grafana/grafana/pkg/build/droneutil" "github.com/urfave/cli/v2" + + "github.com/grafana/grafana/pkg/build/droneutil" ) func GenerateMetadata(c *cli.Context) (Metadata, error) { diff --git a/pkg/build/droneutil/event_test.go b/pkg/build/droneutil/event_test.go index f0efcfa284e..b69ce50fc54 100644 --- a/pkg/build/droneutil/event_test.go +++ b/pkg/build/droneutil/event_test.go @@ -3,8 +3,9 @@ package droneutil_test import ( "testing" - "github.com/grafana/grafana/pkg/build/droneutil" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/build/droneutil" ) func TestGetDroneEvent(t *testing.T) { diff --git a/pkg/build/env/lookup_test.go b/pkg/build/env/lookup_test.go index cebfb4fac29..ccd44b11e8c 100644 --- a/pkg/build/env/lookup_test.go +++ b/pkg/build/env/lookup_test.go @@ -3,9 +3,9 @@ package env_test import ( "testing" - "github.com/grafana/grafana/pkg/build/env" - "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/build/env" ) func TestLookup(t *testing.T) { diff --git a/pkg/build/frontend/config.go b/pkg/build/frontend/config.go index 3bb66238961..5aa6bcc4294 100644 --- a/pkg/build/frontend/config.go +++ b/pkg/build/frontend/config.go @@ -1,8 +1,9 @@ package frontend import ( - "github.com/grafana/grafana/pkg/build/config" "github.com/urfave/cli/v2" + + "github.com/grafana/grafana/pkg/build/config" ) const GrafanaDir = "." diff --git a/pkg/build/fsutil/copy_test.go b/pkg/build/fsutil/copy_test.go index 6c243ee69f7..95cb8e838ae 100644 --- a/pkg/build/fsutil/copy_test.go +++ b/pkg/build/fsutil/copy_test.go @@ -5,9 +5,10 @@ import ( "runtime" "testing" - "github.com/grafana/grafana/pkg/build/fsutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/build/fsutil" ) func TestCopyFile(t *testing.T) { diff --git a/pkg/build/fsutil/exists_test.go b/pkg/build/fsutil/exists_test.go index 1428654b9b8..f46297a0764 100644 --- a/pkg/build/fsutil/exists_test.go +++ b/pkg/build/fsutil/exists_test.go @@ -3,8 +3,9 @@ package fsutil_test import ( "testing" - "github.com/grafana/grafana/pkg/build/fsutil" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/build/fsutil" ) func TestExists_NonExistent(t *testing.T) { diff --git a/pkg/build/gcloud/storage/gsutil.go b/pkg/build/gcloud/storage/gsutil.go index 461fd58c98a..c29bb66bb52 100644 --- a/pkg/build/gcloud/storage/gsutil.go +++ b/pkg/build/gcloud/storage/gsutil.go @@ -16,10 +16,11 @@ import ( "time" "cloud.google.com/go/storage" - "github.com/grafana/grafana/pkg/build/fsutil" - "github.com/grafana/grafana/pkg/build/gcloud" "google.golang.org/api/iterator" "google.golang.org/api/option" + + "github.com/grafana/grafana/pkg/build/fsutil" + "github.com/grafana/grafana/pkg/build/gcloud" ) var ( diff --git a/pkg/build/git/git.go b/pkg/build/git/git.go index 7539a815de6..9fbcc346e92 100644 --- a/pkg/build/git/git.go +++ b/pkg/build/git/git.go @@ -8,8 +8,9 @@ import ( "regexp" "github.com/google/go-github/v45/github" - "github.com/grafana/grafana/pkg/build/stringutil" "golang.org/x/oauth2" + + "github.com/grafana/grafana/pkg/build/stringutil" ) const ( diff --git a/pkg/build/git/git_checks_test.go b/pkg/build/git/git_checks_test.go index ed3d34a8d28..c9bf0c98d0b 100644 --- a/pkg/build/git/git_checks_test.go +++ b/pkg/build/git/git_checks_test.go @@ -6,8 +6,9 @@ import ( "testing" "github.com/google/go-github/v45/github" - "github.com/grafana/grafana/pkg/build/git" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/build/git" ) type TestChecksService struct { diff --git a/pkg/build/git/git_issues_test.go b/pkg/build/git/git_issues_test.go index 4eaa3bb6169..dab9fddde77 100644 --- a/pkg/build/git/git_issues_test.go +++ b/pkg/build/git/git_issues_test.go @@ -6,8 +6,9 @@ import ( "testing" "github.com/google/go-github/v45/github" - "github.com/grafana/grafana/pkg/build/git" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/build/git" ) type TestLabelsService struct { diff --git a/pkg/build/git/git_test.go b/pkg/build/git/git_test.go index c0cbd4c8cd8..3b47ce08dd0 100644 --- a/pkg/build/git/git_test.go +++ b/pkg/build/git/git_test.go @@ -3,8 +3,9 @@ package git_test import ( "testing" - "github.com/grafana/grafana/pkg/build/git" "github.com/stretchr/testify/assert" + + "github.com/grafana/grafana/pkg/build/git" ) func TestPRCheckRegexp(t *testing.T) { diff --git a/pkg/build/metrics/publish.go b/pkg/build/metrics/publish.go index 934f8168802..4e686eb6dea 100644 --- a/pkg/build/metrics/publish.go +++ b/pkg/build/metrics/publish.go @@ -5,10 +5,9 @@ import ( "encoding/json" "fmt" "log" + "net/http" "strconv" "time" - - "net/http" ) type payload struct { diff --git a/pkg/build/packaging/grafana_test.go b/pkg/build/packaging/grafana_test.go index 8f143893085..7f45a632718 100644 --- a/pkg/build/packaging/grafana_test.go +++ b/pkg/build/packaging/grafana_test.go @@ -3,9 +3,10 @@ package packaging_test import ( "testing" + "github.com/stretchr/testify/assert" + "github.com/grafana/grafana/pkg/build/config" "github.com/grafana/grafana/pkg/build/packaging" - "github.com/stretchr/testify/assert" ) func TestPackageRegexp(t *testing.T) { diff --git a/pkg/build/packaging/rpm_test.go b/pkg/build/packaging/rpm_test.go index 5a030a3093b..dfe9e0a18fa 100644 --- a/pkg/build/packaging/rpm_test.go +++ b/pkg/build/packaging/rpm_test.go @@ -5,8 +5,9 @@ import ( "path/filepath" "testing" - "github.com/grafana/grafana/pkg/build/config" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/build/config" ) const pubKey = `-----BEGIN PGP PUBLIC KEY BLOCK----- diff --git a/pkg/bus/bus_test.go b/pkg/bus/bus_test.go index 3e1338e053d..170297797bd 100644 --- a/pkg/bus/bus_test.go +++ b/pkg/bus/bus_test.go @@ -4,8 +4,9 @@ import ( "context" "testing" - "github.com/grafana/grafana/pkg/infra/tracing" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/infra/tracing" ) type testQuery struct { diff --git a/pkg/cmd/grafana-cli/commands/cli.go b/pkg/cmd/grafana-cli/commands/cli.go index eefeb65bc55..4d421fedd6d 100644 --- a/pkg/cmd/grafana-cli/commands/cli.go +++ b/pkg/cmd/grafana-cli/commands/cli.go @@ -4,10 +4,11 @@ import ( "os" "runtime" + "github.com/urfave/cli/v2" + "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" "github.com/grafana/grafana/pkg/cmd/grafana-cli/services" "github.com/grafana/grafana/pkg/cmd/grafana-cli/utils" - "github.com/urfave/cli/v2" ) // RunCLI is the entrypoint for the grafana-cli command. It returns the exit code for the grafana-cli program. diff --git a/pkg/cmd/grafana-cli/commands/commandstest/context.go b/pkg/cmd/grafana-cli/commands/commandstest/context.go index 3da4a8d754c..4ca73bfa873 100644 --- a/pkg/cmd/grafana-cli/commands/commandstest/context.go +++ b/pkg/cmd/grafana-cli/commands/commandstest/context.go @@ -3,8 +3,9 @@ package commandstest import ( "flag" - "github.com/grafana/grafana/pkg/cmd/grafana-cli/utils" "github.com/urfave/cli/v2" + + "github.com/grafana/grafana/pkg/cmd/grafana-cli/utils" ) // NewCliContext creates a new CLI context with a certain set of flags. diff --git a/pkg/cmd/grafana-cli/commands/ls_command.go b/pkg/cmd/grafana-cli/commands/ls_command.go index b23afd48ced..35c7fdd591b 100644 --- a/pkg/cmd/grafana-cli/commands/ls_command.go +++ b/pkg/cmd/grafana-cli/commands/ls_command.go @@ -4,6 +4,7 @@ import ( "errors" "github.com/fatih/color" + "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" "github.com/grafana/grafana/pkg/cmd/grafana-cli/models" "github.com/grafana/grafana/pkg/cmd/grafana-cli/services" diff --git a/pkg/cmd/grafana-cli/commands/upgrade_all_command.go b/pkg/cmd/grafana-cli/commands/upgrade_all_command.go index 9e6e655253e..759b6561bca 100644 --- a/pkg/cmd/grafana-cli/commands/upgrade_all_command.go +++ b/pkg/cmd/grafana-cli/commands/upgrade_all_command.go @@ -3,11 +3,12 @@ package commands import ( "context" + "github.com/hashicorp/go-version" + "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" "github.com/grafana/grafana/pkg/cmd/grafana-cli/models" "github.com/grafana/grafana/pkg/cmd/grafana-cli/services" "github.com/grafana/grafana/pkg/cmd/grafana-cli/utils" - "github.com/hashicorp/go-version" ) func shouldUpgrade(installed string, remote *models.Plugin) bool { diff --git a/pkg/cmd/grafana-cli/commands/upgrade_all_command_test.go b/pkg/cmd/grafana-cli/commands/upgrade_all_command_test.go index 57ea8c15dbc..c69f0a18a08 100644 --- a/pkg/cmd/grafana-cli/commands/upgrade_all_command_test.go +++ b/pkg/cmd/grafana-cli/commands/upgrade_all_command_test.go @@ -4,8 +4,9 @@ import ( "fmt" "testing" - "github.com/grafana/grafana/pkg/cmd/grafana-cli/models" "github.com/stretchr/testify/assert" + + "github.com/grafana/grafana/pkg/cmd/grafana-cli/models" ) func TestVersionComparison(t *testing.T) { diff --git a/pkg/cmd/grafana-cli/commands/upgrade_command.go b/pkg/cmd/grafana-cli/commands/upgrade_command.go index 5e7fffe9037..9e1eb5df6df 100644 --- a/pkg/cmd/grafana-cli/commands/upgrade_command.go +++ b/pkg/cmd/grafana-cli/commands/upgrade_command.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/fatih/color" + "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" "github.com/grafana/grafana/pkg/cmd/grafana-cli/services" "github.com/grafana/grafana/pkg/cmd/grafana-cli/utils" diff --git a/pkg/cmd/grafana-cli/utils/command_line.go b/pkg/cmd/grafana-cli/utils/command_line.go index 31cfc5b2e64..d8faff51969 100644 --- a/pkg/cmd/grafana-cli/utils/command_line.go +++ b/pkg/cmd/grafana-cli/utils/command_line.go @@ -3,8 +3,9 @@ package utils import ( "os" - "github.com/grafana/grafana/pkg/cmd/grafana-cli/models" "github.com/urfave/cli/v2" + + "github.com/grafana/grafana/pkg/cmd/grafana-cli/models" ) type CommandLine interface { diff --git a/pkg/cmd/grafana-server/commands/cli.go b/pkg/cmd/grafana-server/commands/cli.go index 5d8616c0baa..293bdb713c7 100644 --- a/pkg/cmd/grafana-server/commands/cli.go +++ b/pkg/cmd/grafana-server/commands/cli.go @@ -15,6 +15,8 @@ import ( "syscall" "time" + "github.com/urfave/cli/v2" + "github.com/grafana/grafana/pkg/api" "github.com/grafana/grafana/pkg/extensions" "github.com/grafana/grafana/pkg/infra/log" @@ -24,7 +26,6 @@ import ( _ "github.com/grafana/grafana/pkg/services/alerting/conditions" _ "github.com/grafana/grafana/pkg/services/alerting/notifiers" "github.com/grafana/grafana/pkg/setting" - "github.com/urfave/cli/v2" ) type ServerOptions struct { diff --git a/pkg/cmd/grafana/main.go b/pkg/cmd/grafana/main.go index edc58ed5875..b209b916fe8 100644 --- a/pkg/cmd/grafana/main.go +++ b/pkg/cmd/grafana/main.go @@ -5,9 +5,10 @@ import ( "os" "github.com/fatih/color" + "github.com/urfave/cli/v2" + gcli "github.com/grafana/grafana/pkg/cmd/grafana-cli/commands" gsrv "github.com/grafana/grafana/pkg/cmd/grafana-server/commands" - "github.com/urfave/cli/v2" ) // The following variables cannot be constants, since they can be overridden through the -X link flag diff --git a/pkg/expr/classic/classic_test.go b/pkg/expr/classic/classic_test.go index de159f32f21..31ccdbe1425 100644 --- a/pkg/expr/classic/classic_test.go +++ b/pkg/expr/classic/classic_test.go @@ -6,10 +6,10 @@ import ( "testing" "time" + "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/stretchr/testify/require" ptr "github.com/xorcare/pointer" - "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana/pkg/expr/mathexp" ) diff --git a/pkg/expr/classic/evaluator_test.go b/pkg/expr/classic/evaluator_test.go index 4deb92491ad..6cad30ff646 100644 --- a/pkg/expr/classic/evaluator_test.go +++ b/pkg/expr/classic/evaluator_test.go @@ -3,9 +3,10 @@ package classic import ( "testing" - "github.com/grafana/grafana/pkg/expr/mathexp" "github.com/stretchr/testify/require" ptr "github.com/xorcare/pointer" + + "github.com/grafana/grafana/pkg/expr/mathexp" ) func TestThresholdEvaluator(t *testing.T) { diff --git a/pkg/expr/classic/reduce_test.go b/pkg/expr/classic/reduce_test.go index 98cb5e63718..f46798449ed 100644 --- a/pkg/expr/classic/reduce_test.go +++ b/pkg/expr/classic/reduce_test.go @@ -5,10 +5,10 @@ import ( "testing" "time" + "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/stretchr/testify/require" ptr "github.com/xorcare/pointer" - "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana/pkg/expr/mathexp" ) diff --git a/pkg/expr/graph.go b/pkg/expr/graph.go index 9e78e5e2f2e..1d9269d1f93 100644 --- a/pkg/expr/graph.go +++ b/pkg/expr/graph.go @@ -6,10 +6,10 @@ import ( "fmt" "time" - "github.com/grafana/grafana/pkg/expr/mathexp" - "gonum.org/v1/gonum/graph/simple" "gonum.org/v1/gonum/graph/topo" + + "github.com/grafana/grafana/pkg/expr/mathexp" ) // NodeType is the type of a DPNode. Currently either a expression command or datasource query. diff --git a/pkg/expr/nodes.go b/pkg/expr/nodes.go index ab23dabbae0..fca11ec45cb 100644 --- a/pkg/expr/nodes.go +++ b/pkg/expr/nodes.go @@ -8,14 +8,13 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" + "gonum.org/v1/gonum/graph/simple" "github.com/grafana/grafana/pkg/expr/classic" "github.com/grafana/grafana/pkg/expr/mathexp" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/plugins/adapters" "github.com/grafana/grafana/pkg/services/datasources" - - "gonum.org/v1/gonum/graph/simple" ) var ( diff --git a/pkg/infra/appcontext/user.go b/pkg/infra/appcontext/user.go index 604ec008b7a..d2bd57aa000 100644 --- a/pkg/infra/appcontext/user.go +++ b/pkg/infra/appcontext/user.go @@ -4,8 +4,8 @@ import ( "context" "fmt" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" grpccontext "github.com/grafana/grafana/pkg/services/grpcserver/context" "github.com/grafana/grafana/pkg/services/user" ) @@ -33,7 +33,7 @@ func User(ctx context.Context) (*user.SignedInUser, error) { } // Set by incoming HTTP request - c, ok := ctxkey.Get(ctx).(*models.ReqContext) + c, ok := ctxkey.Get(ctx).(*contextmodel.ReqContext) if ok && c.SignedInUser != nil { return c.SignedInUser, nil } diff --git a/pkg/infra/appcontext/user_test.go b/pkg/infra/appcontext/user_test.go index 91913c692f1..6db4735b647 100644 --- a/pkg/infra/appcontext/user_test.go +++ b/pkg/infra/appcontext/user_test.go @@ -6,13 +6,14 @@ import ( "math/big" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" grpccontext "github.com/grafana/grafana/pkg/services/grpcserver/context" "github.com/grafana/grafana/pkg/services/user" - "github.com/stretchr/testify/require" ) func TestUserFromContext(t *testing.T) { @@ -47,7 +48,7 @@ func TestUserFromContext(t *testing.T) { t.Run("should return user set by HTTP ReqContext", func(t *testing.T) { expected := testUser() - ctx := ctxkey.Set(context.Background(), &models.ReqContext{ + ctx := ctxkey.Set(context.Background(), &contextmodel.ReqContext{ SignedInUser: expected, }) actual, err := appcontext.User(ctx) diff --git a/pkg/infra/filestorage/filter.go b/pkg/infra/filestorage/filter.go index db6cadd3fa1..9df3e0c3280 100644 --- a/pkg/infra/filestorage/filter.go +++ b/pkg/infra/filestorage/filter.go @@ -4,6 +4,7 @@ import ( "strings" "github.com/armon/go-radix" + "github.com/grafana/grafana/pkg/services/accesscontrol" ) diff --git a/pkg/infra/log/file_test.go b/pkg/infra/log/file_test.go index f6361e152a3..aaa21f8ead2 100644 --- a/pkg/infra/log/file_test.go +++ b/pkg/infra/log/file_test.go @@ -4,9 +4,8 @@ import ( "os" "testing" - "github.com/stretchr/testify/require" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func (w *FileLogWriter) WriteLine(line string) error { diff --git a/pkg/infra/metrics/metrics.go b/pkg/infra/metrics/metrics.go index c70040c7c40..ee9017cac53 100644 --- a/pkg/infra/metrics/metrics.go +++ b/pkg/infra/metrics/metrics.go @@ -3,10 +3,11 @@ package metrics import ( "runtime" + "github.com/prometheus/client_golang/prometheus" + "github.com/grafana/grafana/pkg/infra/metrics/metricutil" pubdash "github.com/grafana/grafana/pkg/services/publicdashboards/models" "github.com/grafana/grafana/pkg/setting" - "github.com/prometheus/client_golang/prometheus" ) // ExporterName is used as namespace for exposing prometheus metrics diff --git a/pkg/infra/metrics/settings.go b/pkg/infra/metrics/settings.go index cde0e447b28..e07f5533a00 100644 --- a/pkg/infra/metrics/settings.go +++ b/pkg/infra/metrics/settings.go @@ -5,9 +5,10 @@ import ( "strings" "time" + "github.com/prometheus/client_golang/prometheus" + "github.com/grafana/grafana/pkg/infra/metrics/graphitebridge" "github.com/grafana/grafana/pkg/setting" - "github.com/prometheus/client_golang/prometheus" ) func (im *InternalMetricsService) readSettings() error { diff --git a/pkg/infra/remotecache/memcached_storage.go b/pkg/infra/remotecache/memcached_storage.go index b8c1b4e4823..f8601eb1a2d 100644 --- a/pkg/infra/remotecache/memcached_storage.go +++ b/pkg/infra/remotecache/memcached_storage.go @@ -5,6 +5,7 @@ import ( "time" "github.com/bradfitz/gomemcache/memcache" + "github.com/grafana/grafana/pkg/setting" ) diff --git a/pkg/infra/remotecache/redis_storage.go b/pkg/infra/remotecache/redis_storage.go index 7c76345a5bf..f8ed10d5bb8 100644 --- a/pkg/infra/remotecache/redis_storage.go +++ b/pkg/infra/remotecache/redis_storage.go @@ -9,6 +9,7 @@ import ( "time" "github.com/go-redis/redis/v8" + "github.com/grafana/grafana/pkg/setting" ) diff --git a/pkg/infra/tracing/optentelemetry_tracing_test.go b/pkg/infra/tracing/optentelemetry_tracing_test.go index 01e817da776..690edb80070 100644 --- a/pkg/infra/tracing/optentelemetry_tracing_test.go +++ b/pkg/infra/tracing/optentelemetry_tracing_test.go @@ -3,9 +3,10 @@ package tracing import ( "testing" - "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/assert" "go.opentelemetry.io/otel/attribute" + + "github.com/grafana/grafana/pkg/setting" ) func TestSplitCustomAttribs(t *testing.T) { diff --git a/pkg/infra/tracing/tracing_test.go b/pkg/infra/tracing/tracing_test.go index d40390b5276..6c2a01b4c45 100644 --- a/pkg/infra/tracing/tracing_test.go +++ b/pkg/infra/tracing/tracing_test.go @@ -4,9 +4,10 @@ import ( "os" "testing" - "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/setting" ) func TestGroupSplit(t *testing.T) { diff --git a/pkg/infra/usagestats/service/api.go b/pkg/infra/usagestats/service/api.go index 836f73d3202..46ebed121aa 100644 --- a/pkg/infra/usagestats/service/api.go +++ b/pkg/infra/usagestats/service/api.go @@ -6,8 +6,8 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" ) const rootUrl = "/api/admin" @@ -20,7 +20,7 @@ func (uss *UsageStats) registerAPIEndpoints() { }) } -func (uss *UsageStats) getUsageReportPreview(ctx *models.ReqContext) response.Response { +func (uss *UsageStats) getUsageReportPreview(ctx *contextmodel.ReqContext) response.Response { usageReport, err := uss.GetUsageReport(ctx.Req.Context()) if err != nil { return response.Error(http.StatusInternalServerError, "failed to get usage report", err) diff --git a/pkg/infra/usagestats/service/api_test.go b/pkg/infra/usagestats/service/api_test.go index 94acc44e189..92ab183954f 100644 --- a/pkg/infra/usagestats/service/api_test.go +++ b/pkg/infra/usagestats/service/api_test.go @@ -11,8 +11,8 @@ import ( "github.com/grafana/grafana/pkg/infra/db/dbtest" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/stats" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" @@ -93,7 +93,7 @@ type testContext struct { func contextProvider(tc *testContext) web.Handler { return func(c *web.Context) { signedIn := tc.user != nil - reqCtx := &models.ReqContext{ + reqCtx := &contextmodel.ReqContext{ Context: c, SignedInUser: tc.user, IsSignedIn: signedIn, diff --git a/pkg/infra/usagestats/statscollector/prometheus_flavor_test.go b/pkg/infra/usagestats/statscollector/prometheus_flavor_test.go index b0f4cad6d2a..69d85caf58b 100644 --- a/pkg/infra/usagestats/statscollector/prometheus_flavor_test.go +++ b/pkg/infra/usagestats/statscollector/prometheus_flavor_test.go @@ -8,7 +8,6 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/db/dbtest" diff --git a/pkg/infra/usagestats/statscollector/service_test.go b/pkg/infra/usagestats/statscollector/service_test.go index 0200588e3f1..cb8074dedb1 100644 --- a/pkg/infra/usagestats/statscollector/service_test.go +++ b/pkg/infra/usagestats/statscollector/service_test.go @@ -7,13 +7,11 @@ import ( "testing" "time" - "github.com/grafana/grafana/pkg/components/simplejson" - sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/db/dbtest" "github.com/grafana/grafana/pkg/infra/httpclient" diff --git a/pkg/kinds/dashboard/dashboard_types_gen.go b/pkg/kinds/dashboard/dashboard_types_gen.go index 6a34f274e31..9fdc6e22532 100644 --- a/pkg/kinds/dashboard/dashboard_types_gen.go +++ b/pkg/kinds/dashboard/dashboard_types_gen.go @@ -17,15 +17,6 @@ const ( StyleLight Style = "light" ) -// Defines values for Timezone. -const ( - TimezoneBrowser Timezone = "browser" - - TimezoneEmpty Timezone = "" - - TimezoneUtc Timezone = "utc" -) - // Defines values for CursorSync. const ( CursorSyncN0 CursorSync = 0 @@ -290,8 +281,8 @@ type Dashboard struct { TimeOptions []string `json:"time_options"` } `json:"timepicker,omitempty"` - // Timezone of dashboard, - Timezone *Timezone `json:"timezone,omitempty"` + // Timezone of dashboard. Accepts IANA TZDB zone ID or "browser" or "utc". + Timezone *string `json:"timezone,omitempty"` // Title of dashboard. Title *string `json:"title,omitempty"` @@ -309,9 +300,6 @@ type Dashboard struct { // Theme of dashboard. type Style string -// Timezone of dashboard, -type Timezone string - // 0 for no shared crosshair or tooltip (default). // 1 for shared crosshair. // 2 for shared crosshair AND shared tooltip. @@ -470,6 +458,12 @@ type HeatmapPanel struct { // HeatmapPanelType defines model for HeatmapPanel.Type. type HeatmapPanelType string +// LibraryPanelRef defines model for LibraryPanelRef. +type LibraryPanelRef struct { + Name string `json:"name"` + Uid string `json:"uid"` +} + // LoadingState defines model for LoadingState. type LoadingState string @@ -502,7 +496,8 @@ type Panel struct { // TODO docs // TODO tighter constraint - Interval *string `json:"interval,omitempty"` + Interval *string `json:"interval,omitempty"` + LibraryPanel *LibraryPanelRef `json:"libraryPanel,omitempty"` // Panel links. // TODO fill this out - seems there are a couple variants? diff --git a/pkg/kinds/librarypanel/librarypanel_kind_gen.go b/pkg/kinds/librarypanel/librarypanel_kind_gen.go new file mode 100644 index 00000000000..564ddfb169c --- /dev/null +++ b/pkg/kinds/librarypanel/librarypanel_kind_gen.go @@ -0,0 +1,113 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. +// +// Generated by: +// kinds/gen.go +// Using jennies: +// CoreKindJenny +// +// Run 'make gen-cue' from repository root to regenerate. + +package librarypanel + +import ( + "github.com/grafana/grafana/pkg/kindsys" + "github.com/grafana/thema" + "github.com/grafana/thema/vmux" +) + +// rootrel is the relative path from the grafana repository root to the +// directory containing the .cue files in which this kind is declared. Necessary +// for runtime errors related to the declaration and/or lineage to provide +// a real path to the correct .cue file. +const rootrel string = "kinds/librarypanel" + +// TODO standard generated docs +type Kind struct { + lin thema.ConvergentLineage[*LibraryPanel] + jcodec vmux.Codec + valmux vmux.ValueMux[*LibraryPanel] + decl kindsys.Decl[kindsys.CoreProperties] +} + +// type guard +var _ kindsys.Core = &Kind{} + +// TODO standard generated docs +func NewKind(rt *thema.Runtime, opts ...thema.BindOption) (*Kind, error) { + decl, err := kindsys.LoadCoreKind(rootrel, rt.Context(), nil) + if err != nil { + return nil, err + } + k := &Kind{ + decl: decl, + } + + lin, err := decl.Some().BindKindLineage(rt, opts...) + if err != nil { + return nil, err + } + + // Get the thema.Schema that the meta says is in the current version (which + // codegen ensures is always the latest) + cursch := thema.SchemaP(lin, k.decl.Properties.CurrentVersion) + tsch, err := thema.BindType[*LibraryPanel](cursch, &LibraryPanel{}) + if err != nil { + // Should be unreachable, modulo bugs in the Thema->Go code generator + return nil, err + } + + k.jcodec = vmux.NewJSONCodec("librarypanel.json") + k.lin = tsch.ConvergentLineage() + k.valmux = vmux.NewValueMux(k.lin.TypedSchema(), k.jcodec) + return k, nil +} + +// TODO standard generated docs +func (k *Kind) Name() string { + return "librarypanel" +} + +// TODO standard generated docs +func (k *Kind) MachineName() string { + return "librarypanel" +} + +// TODO standard generated docs +func (k *Kind) Lineage() thema.Lineage { + return k.lin +} + +// TODO standard generated docs +func (k *Kind) ConvergentLineage() thema.ConvergentLineage[*LibraryPanel] { + return k.lin +} + +// JSONValueMux is a version multiplexer that maps a []byte containing JSON data +// at any schematized dashboard version to an instance of LibraryPanel. +// +// Validation and translation errors emitted from this func will identify the +// input bytes as "dashboard.json". +// +// This is a thin wrapper around Thema's [vmux.ValueMux]. +func (k *Kind) JSONValueMux(b []byte) (*LibraryPanel, thema.TranslationLacunas, error) { + return k.valmux(b) +} + +// TODO standard generated docs +func (k *Kind) Maturity() kindsys.Maturity { + return k.decl.Properties.Maturity +} + +// Decl returns the [kindsys.Decl] containing both CUE and Go representations of the +// librarypanel declaration in .cue files. +func (k *Kind) Decl() kindsys.Decl[kindsys.CoreProperties] { + return k.decl +} + +// Props returns a [kindsys.SomeKindProps], with underlying type [kindsys.CoreProperties], +// representing the static properties declared in the librarypanel kind. +// +// This method is identical to calling Decl().Props. It is provided to satisfy [kindsys.Interface]. +func (k *Kind) Props() kindsys.SomeKindProperties { + return k.decl.Properties +} diff --git a/pkg/kinds/librarypanel/librarypanel_types_gen.go b/pkg/kinds/librarypanel/librarypanel_types_gen.go new file mode 100644 index 00000000000..bdd4545030d --- /dev/null +++ b/pkg/kinds/librarypanel/librarypanel_types_gen.go @@ -0,0 +1,58 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. +// +// Generated by: +// kinds/gen.go +// Using jennies: +// GoTypesJenny +// LatestJenny +// +// Run 'make gen-cue' from repository root to regenerate. + +package librarypanel + +// LibraryElementDTOMeta defines model for LibraryElementDTOMeta. +type LibraryElementDTOMeta struct { + ConnectedDashboards int64 `json:"connectedDashboards"` + Created string `json:"created"` + CreatedBy LibraryElementDTOMetaUser `json:"createdBy"` + FolderName string `json:"folderName"` + FolderUid string `json:"folderUid"` + Updated string `json:"updated"` + UpdatedBy LibraryElementDTOMetaUser `json:"updatedBy"` +} + +// LibraryElementDTOMetaUser defines model for LibraryElementDTOMetaUser. +type LibraryElementDTOMetaUser struct { + AvatarUrl string `json:"avatarUrl"` + Id int64 `json:"id"` + Name string `json:"name"` +} + +// LibraryPanel defines model for LibraryPanel. +type LibraryPanel struct { + // Panel description + Description *string `json:"description,omitempty"` + + // Folder UID + FolderUid *string `json:"folderUid,omitempty"` + Meta *LibraryElementDTOMeta `json:"meta,omitempty"` + + // TODO: should be the same panel schema defined in dashboard + // Typescript: Omit; + Model map[string]interface{} `json:"model"` + + // Panel name (also saved in the model) + Name string `json:"name"` + + // Dashboard version when this was saved (zero if unknown) + SchemaVersion *int `json:"schemaVersion,omitempty"` + + // The panel type (from inside the model) + Type string `json:"type"` + + // Library element UID + Uid string `json:"uid"` + + // panel version, incremented each time the dashboard is updated. + Version int64 `json:"version"` +} diff --git a/pkg/kindsys/report.json b/pkg/kindsys/report.json index d9fd1cf56d6..0944a34047f 100644 --- a/pkg/kindsys/report.json +++ b/pkg/kindsys/report.json @@ -283,7 +283,7 @@ 0, 0 ], - "grafanaMaturityCount": 140, + "grafanaMaturityCount": 139, "lineageIsGroup": false, "links": { "docs": "https://grafana.com/docs/grafana/next/developers/kinds/core/dashboard/schema-reference", @@ -807,6 +807,32 @@ "pluralName": "JaegerDataSourceCfgs", "schemaInterface": "DataSourceCfg" }, + "librarypanel": { + "category": "core", + "codeowners": [ + "grafana/grafana-as-code", + "grafana/grafana-bi-squad", + "grafana/plugins-platform-frontend", + "grafana/user-essentials" + ], + "currentVersion": [ + 0, + 0 + ], + "grafanaMaturityCount": 10, + "lineageIsGroup": false, + "links": { + "docs": "https://grafana.com/docs/grafana/next/developers/kinds/core/librarypanel/schema-reference", + "go": "https://github.com/grafana/grafana/tree/main/pkg/kinds/librarypanel", + "schema": "https://github.com/grafana/grafana/tree/main/kinds/librarypanel/librarypanel_kind.cue", + "ts": "https://github.com/grafana/grafana/tree/main/packages/grafana-schema/src/raw/librarypanel/x/librarypanel_types.gen.ts" + }, + "machineName": "librarypanel", + "maturity": "experimental", + "name": "LibraryPanel", + "pluralMachineName": "librarypanels", + "pluralName": "LibraryPanels" + }, "livepanelcfg": { "category": "composable", "codeowners": [], @@ -1846,6 +1872,7 @@ "dashboard", "datasource", "folder", + "librarypanel", "playlist", "preferences", "publicdashboard", @@ -1856,7 +1883,7 @@ "thumb", "user" ], - "count": 13 + "count": 14 } }, "maturity": { @@ -1870,6 +1897,7 @@ "dashboardlistpanelcfg", "gaugepanelcfg", "histogrampanelcfg", + "librarypanel", "newspanelcfg", "nodegraphpanelcfg", "piechartpanelcfg", @@ -1880,7 +1908,7 @@ "textpanelcfg", "xychartpanelcfg" ], - "count": 16 + "count": 17 }, "mature": { "name": "mature", diff --git a/pkg/login/auth.go b/pkg/login/auth.go index 382b4b4f288..0a22319f0f9 100644 --- a/pkg/login/auth.go +++ b/pkg/login/auth.go @@ -6,7 +6,6 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/ldap" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/loginattempt" @@ -30,7 +29,7 @@ var ( var loginLogger = log.New("login") type Authenticator interface { - AuthenticateUser(context.Context, *models.LoginUserQuery) error + AuthenticateUser(context.Context, *login.LoginUserQuery) error } type AuthenticatorService struct { @@ -49,7 +48,7 @@ func ProvideService(store db.DB, loginService login.Service, loginAttemptService } // AuthenticateUser authenticates the user via username & password -func (a *AuthenticatorService) AuthenticateUser(ctx context.Context, query *models.LoginUserQuery) error { +func (a *AuthenticatorService) AuthenticateUser(ctx context.Context, query *login.LoginUserQuery) error { ok, err := a.loginAttemptService.Validate(ctx, query.Username) if err != nil { return err diff --git a/pkg/login/auth_test.go b/pkg/login/auth_test.go index edc8f4ed3d8..8720c6e0ea4 100644 --- a/pkg/login/auth_test.go +++ b/pkg/login/auth_test.go @@ -5,15 +5,15 @@ import ( "errors" "testing" - "github.com/grafana/grafana/pkg/models" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/services/ldap" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/login/logintest" "github.com/grafana/grafana/pkg/services/loginattempt/loginattempttest" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestAuthenticateUser(t *testing.T) { @@ -23,7 +23,7 @@ func TestAuthenticateUser(t *testing.T) { loginAttemptService := &loginattempttest.FakeLoginAttemptService{ExpectedValid: true} a := AuthenticatorService{loginAttemptService: loginAttemptService, loginService: &logintest.LoginServiceFake{}} - err := a.AuthenticateUser(context.Background(), &models.LoginUserQuery{ + err := a.AuthenticateUser(context.Background(), &login.LoginUserQuery{ Username: "user", Password: "", }) @@ -180,7 +180,7 @@ func TestAuthenticateUser(t *testing.T) { } type authScenarioContext struct { - loginUserQuery *models.LoginUserQuery + loginUserQuery *login.LoginUserQuery grafanaLoginWasCalled bool ldapLoginWasCalled bool } @@ -188,14 +188,14 @@ type authScenarioContext struct { type authScenarioFunc func(sc *authScenarioContext) func mockLoginUsingGrafanaDB(err error, sc *authScenarioContext) { - loginUsingGrafanaDB = func(ctx context.Context, query *models.LoginUserQuery, _ user.Service) error { + loginUsingGrafanaDB = func(ctx context.Context, query *login.LoginUserQuery, _ user.Service) error { sc.grafanaLoginWasCalled = true return err } } func mockLoginUsingLDAP(enabled bool, err error, sc *authScenarioContext) { - loginUsingLDAP = func(ctx context.Context, query *models.LoginUserQuery, _ login.Service) (bool, error) { + loginUsingLDAP = func(ctx context.Context, query *login.LoginUserQuery, _ login.Service) (bool, error) { sc.ldapLoginWasCalled = true return enabled, err } @@ -209,7 +209,7 @@ func authScenario(t *testing.T, desc string, fn authScenarioFunc) { origLoginUsingLDAP := loginUsingLDAP cfg := setting.Cfg{DisableLogin: false} sc := &authScenarioContext{ - loginUserQuery: &models.LoginUserQuery{ + loginUserQuery: &login.LoginUserQuery{ Username: "user", Password: "pwd", IpAddress: "192.168.1.1:56433", diff --git a/pkg/login/grafana_login.go b/pkg/login/grafana_login.go index 64bc19362e1..3ef14a39f79 100644 --- a/pkg/login/grafana_login.go +++ b/pkg/login/grafana_login.go @@ -4,7 +4,7 @@ import ( "context" "crypto/subtle" - "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/util" ) @@ -21,7 +21,7 @@ var validatePassword = func(providedPassword string, userPassword string, userSa return nil } -var loginUsingGrafanaDB = func(ctx context.Context, query *models.LoginUserQuery, userService user.Service) error { +var loginUsingGrafanaDB = func(ctx context.Context, query *login.LoginUserQuery, userService user.Service) error { userQuery := user.GetUserByLoginQuery{LoginOrEmail: query.Username} user, err := userService.GetByLogin(ctx, &userQuery) diff --git a/pkg/login/grafana_login_test.go b/pkg/login/grafana_login_test.go index 531f6a1dcc3..cd68019de97 100644 --- a/pkg/login/grafana_login_test.go +++ b/pkg/login/grafana_login_test.go @@ -9,7 +9,7 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/db/dbtest" - "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/usertest" ) @@ -59,7 +59,7 @@ func TestLoginUsingGrafanaDB(t *testing.T) { type grafanaLoginScenarioContext struct { store db.DB userService *usertest.FakeUserService - loginUserQuery *models.LoginUserQuery + loginUserQuery *login.LoginUserQuery validatePasswordCalled bool } @@ -73,7 +73,7 @@ func grafanaLoginScenario(t *testing.T, desc string, fn grafanaLoginScenarioFunc sc := &grafanaLoginScenarioContext{ store: dbtest.NewFakeDB(), - loginUserQuery: &models.LoginUserQuery{ + loginUserQuery: &login.LoginUserQuery{ Username: "user", Password: "pwd", IpAddress: "192.168.1.1:56433", diff --git a/pkg/login/ldap_login.go b/pkg/login/ldap_login.go index bf2b23a9e24..a2f0b4f3321 100644 --- a/pkg/login/ldap_login.go +++ b/pkg/login/ldap_login.go @@ -6,7 +6,6 @@ import ( "fmt" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/ldap" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/multildap" @@ -27,7 +26,7 @@ var ldapLogger = log.New("login.ldap") // loginUsingLDAP logs in user using LDAP. It returns whether LDAP is enabled and optional error and query arg will be // populated with the logged in user if successful. -var loginUsingLDAP = func(ctx context.Context, query *models.LoginUserQuery, loginService login.Service) (bool, error) { +var loginUsingLDAP = func(ctx context.Context, query *login.LoginUserQuery, loginService login.Service) (bool, error) { enabled := isLDAPEnabled() if !enabled { @@ -54,11 +53,11 @@ var loginUsingLDAP = func(ctx context.Context, query *models.LoginUserQuery, log return true, err } - upsert := &models.UpsertUserCommand{ + upsert := &login.UpsertUserCommand{ ReqContext: query.ReqContext, ExternalUser: externalUser, SignupAllowed: setting.LDAPAllowSignup, - UserLookupParams: models.UserLookupParams{ + UserLookupParams: login.UserLookupParams{ Login: &externalUser.Login, Email: &externalUser.Email, UserID: nil, diff --git a/pkg/login/ldap_login_test.go b/pkg/login/ldap_login_test.go index 4503b272e92..ac4dc47205c 100644 --- a/pkg/login/ldap_login_test.go +++ b/pkg/login/ldap_login_test.go @@ -5,13 +5,14 @@ import ( "errors" "testing" - "github.com/grafana/grafana/pkg/models" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/services/ldap" + "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/login/logintest" "github.com/grafana/grafana/pkg/services/multildap" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) var errTest = errors.New("test error") @@ -62,8 +63,8 @@ func (auth *mockAuth) Ping() ([]*multildap.ServerStatus, error) { return nil, nil } -func (auth *mockAuth) Login(query *models.LoginUserQuery) ( - *models.ExternalUserInfo, +func (auth *mockAuth) Login(query *login.LoginUserQuery) ( + *login.ExternalUserInfo, error, ) { auth.loginCalled = true @@ -76,14 +77,14 @@ func (auth *mockAuth) Login(query *models.LoginUserQuery) ( } func (auth *mockAuth) Users(logins []string) ( - []*models.ExternalUserInfo, + []*login.ExternalUserInfo, error, ) { return nil, nil } func (auth *mockAuth) User(login string) ( - *models.ExternalUserInfo, + *login.ExternalUserInfo, ldap.ServerConfig, error, ) { @@ -111,7 +112,7 @@ func mockLDAPAuthenticator(valid bool) *mockAuth { } type LDAPLoginScenarioContext struct { - loginUserQuery *models.LoginUserQuery + loginUserQuery *login.LoginUserQuery LDAPAuthenticatorMock *mockAuth } @@ -124,7 +125,7 @@ func LDAPLoginScenario(t *testing.T, desc string, fn LDAPLoginScenarioFunc) { mock := &mockAuth{} sc := &LDAPLoginScenarioContext{ - loginUserQuery: &models.LoginUserQuery{ + loginUserQuery: &login.LoginUserQuery{ Username: "user", Password: "pwd", IpAddress: "192.168.1.1:56433", diff --git a/pkg/login/social/azuread_oauth.go b/pkg/login/social/azuread_oauth.go index 8f10f8e812c..c5120818d70 100644 --- a/pkg/login/social/azuread_oauth.go +++ b/pkg/login/social/azuread_oauth.go @@ -8,11 +8,11 @@ import ( "net/http" "strings" - "github.com/grafana/grafana/pkg/models/roletype" - "github.com/grafana/grafana/pkg/services/org" - "golang.org/x/oauth2" "gopkg.in/square/go-jose.v2/jwt" + + "github.com/grafana/grafana/pkg/models/roletype" + "github.com/grafana/grafana/pkg/services/org" ) type SocialAzureAD struct { diff --git a/pkg/login/social/generic_oauth_test.go b/pkg/login/social/generic_oauth_test.go index c11d2fde527..40f19e3a3d9 100644 --- a/pkg/login/social/generic_oauth_test.go +++ b/pkg/login/social/generic_oauth_test.go @@ -7,11 +7,11 @@ import ( "testing" "time" + "github.com/go-kit/log/level" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/oauth2" - "github.com/go-kit/log/level" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/org" ) diff --git a/pkg/login/social/gitlab_oauth_test.go b/pkg/login/social/gitlab_oauth_test.go index 188b23e7d44..a4eb8480447 100644 --- a/pkg/login/social/gitlab_oauth_test.go +++ b/pkg/login/social/gitlab_oauth_test.go @@ -6,8 +6,9 @@ import ( "strings" "testing" - "github.com/grafana/grafana/pkg/services/org" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/services/org" ) const ( diff --git a/pkg/login/social/grafana_com_oauth.go b/pkg/login/social/grafana_com_oauth.go index 7db69cf0aa3..7ac1568cd1e 100644 --- a/pkg/login/social/grafana_com_oauth.go +++ b/pkg/login/social/grafana_com_oauth.go @@ -5,10 +5,10 @@ import ( "fmt" "net/http" + "golang.org/x/oauth2" + "github.com/grafana/grafana/pkg/models/roletype" "github.com/grafana/grafana/pkg/services/org" - - "golang.org/x/oauth2" ) type SocialGrafanaCom struct { diff --git a/pkg/login/social/okta_oauth.go b/pkg/login/social/okta_oauth.go index 6eda8afab24..0cfe488e85c 100644 --- a/pkg/login/social/okta_oauth.go +++ b/pkg/login/social/okta_oauth.go @@ -8,12 +8,15 @@ import ( "golang.org/x/oauth2" "gopkg.in/square/go-jose.v2/jwt" + + "github.com/grafana/grafana/pkg/models/roletype" ) type SocialOkta struct { *SocialBase - apiUrl string - allowedGroups []string + apiUrl string + allowedGroups []string + skipOrgRoleSync bool } type OktaUserInfoJson struct { @@ -75,14 +78,20 @@ func (s *SocialOkta) UserInfo(client *http.Client, token *oauth2.Token) (*BasicU return nil, errMissingGroupMembership } - role, grafanaAdmin := s.extractRoleAndAdmin(data.rawJSON, groups, true) - if s.roleAttributeStrict && !role.IsValid() { - return nil, &InvalidBasicRoleError{idP: "Okta", assignedRole: string(role)} + var role roletype.RoleType + var isGrafanaAdmin *bool + if !s.skipOrgRoleSync { + var grafanaAdmin bool + role, grafanaAdmin = s.extractRoleAndAdmin(data.rawJSON, groups, true) + if s.roleAttributeStrict && !role.IsValid() { + return nil, &InvalidBasicRoleError{idP: "Okta", assignedRole: string(role)} + } + if s.allowAssignGrafanaAdmin { + isGrafanaAdmin = &grafanaAdmin + } } - - var isGrafanaAdmin *bool = nil - if s.allowAssignGrafanaAdmin { - isGrafanaAdmin = &grafanaAdmin + if s.allowAssignGrafanaAdmin && s.skipOrgRoleSync { + s.log.Debug("allowAssignGrafanaAdmin and skipOrgRoleSync are both set, Grafana Admin role will not be synced, consider setting one or the other") } return &BasicUserInfo{ diff --git a/pkg/login/social/social.go b/pkg/login/social/social.go index af56b006c4b..5ff006a5827 100644 --- a/pkg/login/social/social.go +++ b/pkg/login/social/social.go @@ -1,6 +1,7 @@ package social import ( + "context" "crypto/tls" "crypto/x509" "encoding/json" @@ -9,8 +10,6 @@ import ( "os" "strings" - "context" - "golang.org/x/oauth2" "golang.org/x/text/cases" "golang.org/x/text/language" @@ -184,9 +183,10 @@ func ProvideService(cfg *setting.Cfg, // Okta if name == "okta" { ss.socialMap["okta"] = &SocialOkta{ - SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features), - apiUrl: info.ApiUrl, - allowedGroups: util.SplitString(sec.Key("allowed_groups").String()), + SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features), + apiUrl: info.ApiUrl, + allowedGroups: util.SplitString(sec.Key("allowed_groups").String()), + skipOrgRoleSync: cfg.OktaSkipOrgRoleSync, } } diff --git a/pkg/middleware/auth.go b/pkg/middleware/auth.go index aeef297dc16..8ac2e457c7d 100644 --- a/pkg/middleware/auth.go +++ b/pkg/middleware/auth.go @@ -10,9 +10,9 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/middleware/cookies" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/auth" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/org" @@ -27,7 +27,7 @@ type AuthOptions struct { ReqNoAnonynmous bool } -func accessForbidden(c *models.ReqContext) { +func accessForbidden(c *contextmodel.ReqContext) { if c.IsApiRequest() { c.JsonApiErr(403, "Permission denied", nil) return @@ -36,7 +36,7 @@ func accessForbidden(c *models.ReqContext) { c.Redirect(setting.AppSubUrl + "/") } -func notAuthorized(c *models.ReqContext) { +func notAuthorized(c *contextmodel.ReqContext) { if c.IsApiRequest() { c.WriteErrOrFallback(http.StatusUnauthorized, http.StatusText(http.StatusUnauthorized), c.LookupTokenErr) return @@ -46,7 +46,7 @@ func notAuthorized(c *models.ReqContext) { c.Redirect(setting.AppSubUrl + "/login") } -func tokenRevoked(c *models.ReqContext, err *auth.TokenRevokedError) { +func tokenRevoked(c *contextmodel.ReqContext, err *auth.TokenRevokedError) { if c.IsApiRequest() { c.JSON(401, map[string]interface{}{ "message": "Token revoked", @@ -62,7 +62,7 @@ func tokenRevoked(c *models.ReqContext, err *auth.TokenRevokedError) { c.Redirect(setting.AppSubUrl + "/login") } -func writeRedirectCookie(c *models.ReqContext) { +func writeRedirectCookie(c *contextmodel.ReqContext) { redirectTo := c.Req.RequestURI if setting.AppSubUrl != "" && !strings.HasPrefix(redirectTo, setting.AppSubUrl) { redirectTo = setting.AppSubUrl + c.Req.RequestURI @@ -83,14 +83,14 @@ func removeForceLoginParams(str string) string { return forceLoginParamsRegexp.ReplaceAllString(str, "") } -func EnsureEditorOrViewerCanEdit(c *models.ReqContext) { +func EnsureEditorOrViewerCanEdit(c *contextmodel.ReqContext) { if !c.SignedInUser.HasRole(org.RoleEditor) && !setting.ViewersCanEdit { accessForbidden(c) } } -func CanAdminPlugins(cfg *setting.Cfg) func(c *models.ReqContext) { - return func(c *models.ReqContext) { +func CanAdminPlugins(cfg *setting.Cfg) func(c *contextmodel.ReqContext) { + return func(c *contextmodel.ReqContext) { if !plugins.ReqCanAdminPlugins(cfg)(c) { accessForbidden(c) return @@ -99,7 +99,7 @@ func CanAdminPlugins(cfg *setting.Cfg) func(c *models.ReqContext) { } func RoleAuth(roles ...org.RoleType) web.Handler { - return func(c *models.ReqContext) { + return func(c *contextmodel.ReqContext) { ok := false for _, role := range roles { if role == c.OrgRole { @@ -114,7 +114,7 @@ func RoleAuth(roles ...org.RoleType) web.Handler { } func Auth(options *AuthOptions) web.Handler { - return func(c *models.ReqContext) { + return func(c *contextmodel.ReqContext) { forceLogin := false if c.AllowAnonymous { forceLogin = shouldForceLogin(c) @@ -153,7 +153,7 @@ func Auth(options *AuthOptions) web.Handler { // Intended for when feature flags open up access to APIs that // are otherwise only available to admins. func AdminOrEditorAndFeatureEnabled(enabled bool) web.Handler { - return func(c *models.ReqContext) { + return func(c *contextmodel.ReqContext) { if c.OrgRole == org.RoleAdmin { return } @@ -169,7 +169,7 @@ func AdminOrEditorAndFeatureEnabled(enabled bool) web.Handler { // SnapshotPublicModeOrSignedIn creates a middleware that allows access // if snapshot public mode is enabled or if user is signed in. func SnapshotPublicModeOrSignedIn(cfg *setting.Cfg) web.Handler { - return func(c *models.ReqContext) { + return func(c *contextmodel.ReqContext) { if cfg.SnapshotPublicMode { return } @@ -181,7 +181,7 @@ func SnapshotPublicModeOrSignedIn(cfg *setting.Cfg) web.Handler { } } -func ReqNotSignedIn(c *models.ReqContext) { +func ReqNotSignedIn(c *contextmodel.ReqContext) { if c.IsSignedIn { c.Redirect(setting.AppSubUrl + "/") } @@ -190,7 +190,7 @@ func ReqNotSignedIn(c *models.ReqContext) { // NoAuth creates a middleware that doesn't require any authentication. // If forceLogin param is set it will redirect the user to the login page. func NoAuth() web.Handler { - return func(c *models.ReqContext) { + return func(c *contextmodel.ReqContext) { if shouldForceLogin(c) { notAuthorized(c) return @@ -200,7 +200,7 @@ func NoAuth() web.Handler { // shouldForceLogin checks if user should be enforced to login. // Returns true if forceLogin parameter is set. -func shouldForceLogin(c *models.ReqContext) bool { +func shouldForceLogin(c *contextmodel.ReqContext) bool { forceLogin := false forceLoginParam, err := strconv.ParseBool(c.Req.URL.Query().Get("forceLogin")) if err == nil { @@ -210,8 +210,8 @@ func shouldForceLogin(c *models.ReqContext) bool { return forceLogin } -func OrgAdminDashOrFolderAdminOrTeamAdmin(ss db.DB, ds dashboards.DashboardService, ts team.Service) func(c *models.ReqContext) { - return func(c *models.ReqContext) { +func OrgAdminDashOrFolderAdminOrTeamAdmin(ss db.DB, ds dashboards.DashboardService, ts team.Service) func(c *contextmodel.ReqContext) { + return func(c *contextmodel.ReqContext) { if c.OrgRole == org.RoleAdmin { return } diff --git a/pkg/middleware/auth_test.go b/pkg/middleware/auth_test.go index bcef0a863e6..9e12a2ffd64 100644 --- a/pkg/middleware/auth_test.go +++ b/pkg/middleware/auth_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/setting" ) @@ -87,7 +87,7 @@ func TestMiddlewareAuth(t *testing.T) { middlewareScenario(t, "Snapshot public mode disabled and unauthenticated request should return 401", func( t *testing.T, sc *scenarioContext) { - sc.m.Get("/api/snapshot", func(c *models.ReqContext) { + sc.m.Get("/api/snapshot", func(c *contextmodel.ReqContext) { c.IsSignedIn = false }, SnapshotPublicModeOrSignedIn(sc.cfg), sc.defaultHandler) sc.fakeReq("GET", "/api/snapshot").exec() @@ -96,7 +96,7 @@ func TestMiddlewareAuth(t *testing.T) { middlewareScenario(t, "Snapshot public mode disabled and authenticated request should return 200", func( t *testing.T, sc *scenarioContext) { - sc.m.Get("/api/snapshot", func(c *models.ReqContext) { + sc.m.Get("/api/snapshot", func(c *contextmodel.ReqContext) { c.IsSignedIn = true }, SnapshotPublicModeOrSignedIn(sc.cfg), sc.defaultHandler) sc.fakeReq("GET", "/api/snapshot").exec() diff --git a/pkg/middleware/cookies/cookies.go b/pkg/middleware/cookies/cookies.go index 3d068cff2aa..915e396a8e6 100644 --- a/pkg/middleware/cookies/cookies.go +++ b/pkg/middleware/cookies/cookies.go @@ -5,7 +5,7 @@ import ( "net/url" "time" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/setting" ) @@ -55,7 +55,7 @@ func WriteCookie(w http.ResponseWriter, name string, value string, maxAge int, g http.SetCookie(w, &cookie) } -func WriteSessionCookie(ctx *models.ReqContext, cfg *setting.Cfg, value string, maxLifetime time.Duration) { +func WriteSessionCookie(ctx *contextmodel.ReqContext, cfg *setting.Cfg, value string, maxLifetime time.Duration) { if cfg.Env == setting.Dev { ctx.Logger.Info("New token", "unhashed token", value) } diff --git a/pkg/middleware/dashboard_redirect.go b/pkg/middleware/dashboard_redirect.go index 496f8243e81..5409cfa2179 100644 --- a/pkg/middleware/dashboard_redirect.go +++ b/pkg/middleware/dashboard_redirect.go @@ -4,14 +4,14 @@ import ( "fmt" "strings" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/setting" ) // In Grafana v7.0 we changed panel edit & view query parameters. // This middleware tries to detect those old url parameters and direct to the new url query params -func RedirectFromLegacyPanelEditURL(cfg *setting.Cfg) func(c *models.ReqContext) { - return func(c *models.ReqContext) { +func RedirectFromLegacyPanelEditURL(cfg *setting.Cfg) func(c *contextmodel.ReqContext) { + return func(c *contextmodel.ReqContext) { queryParams := c.Req.URL.Query() panelID, hasPanelID := queryParams["panelId"] diff --git a/pkg/middleware/logger.go b/pkg/middleware/logger.go index 4366ca45e45..83d78b82ec9 100644 --- a/pkg/middleware/logger.go +++ b/pkg/middleware/logger.go @@ -21,8 +21,8 @@ import ( "time" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/contexthandler" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web" @@ -89,7 +89,7 @@ var sensitiveQueryStrings = [...]string{ "auth_token", } -func SanitizeURL(ctx *models.ReqContext, s string) string { +func SanitizeURL(ctx *contextmodel.ReqContext, s string) string { if s == "" { return s } diff --git a/pkg/middleware/logger_test.go b/pkg/middleware/logger_test.go index 1b74bb7246c..a18033960d0 100644 --- a/pkg/middleware/logger_test.go +++ b/pkg/middleware/logger_test.go @@ -3,14 +3,15 @@ package middleware import ( "testing" - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/stretchr/testify/assert" + + "github.com/grafana/grafana/pkg/infra/log" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" ) func Test_sanitizeURL(t *testing.T) { type args struct { - ctx *models.ReqContext + ctx *contextmodel.ReqContext s string } tests := []struct { @@ -21,7 +22,7 @@ func Test_sanitizeURL(t *testing.T) { { name: "Receiving empty string should return it", args: args{ - ctx: &models.ReqContext{ + ctx: &contextmodel.ReqContext{ Logger: log.New("test.logger"), }, s: "", @@ -31,7 +32,7 @@ func Test_sanitizeURL(t *testing.T) { { name: "Receiving valid URL string should return it parsed", args: args{ - ctx: &models.ReqContext{ + ctx: &contextmodel.ReqContext{ Logger: log.New("test.logger"), }, s: "https://grafana.com/", @@ -41,7 +42,7 @@ func Test_sanitizeURL(t *testing.T) { { name: "Receiving invalid URL string should return empty string", args: args{ - ctx: &models.ReqContext{ + ctx: &contextmodel.ReqContext{ Logger: log.New("test.logger"), }, s: "this is not a valid URL", diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index 9bb6c6cb835..2bc0893e5b7 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -4,7 +4,7 @@ import ( "fmt" "strings" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web" @@ -21,7 +21,7 @@ var ( ReqOrgAdmin = RoleAuth(org.RoleAdmin) ) -func HandleNoCacheHeader(ctx *models.ReqContext) { +func HandleNoCacheHeader(ctx *contextmodel.ReqContext) { ctx.SkipCache = ctx.Req.Header.Get("X-Grafana-NoCache") == "true" } diff --git a/pkg/middleware/middleware_basic_auth_test.go b/pkg/middleware/middleware_basic_auth_test.go index 215a819f79e..e02e2f8e92f 100644 --- a/pkg/middleware/middleware_basic_auth_test.go +++ b/pkg/middleware/middleware_basic_auth_test.go @@ -4,6 +4,9 @@ import ( "encoding/json" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/login" "github.com/grafana/grafana/pkg/services/apikey" "github.com/grafana/grafana/pkg/services/contexthandler" @@ -12,8 +15,6 @@ import ( "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestMiddlewareBasicAuth(t *testing.T) { diff --git a/pkg/middleware/middleware_jwt_auth_test.go b/pkg/middleware/middleware_jwt_auth_test.go index cc3b48c001a..3af4eaf88b6 100644 --- a/pkg/middleware/middleware_jwt_auth_test.go +++ b/pkg/middleware/middleware_jwt_auth_test.go @@ -9,9 +9,8 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/services/auth/jwt" - "github.com/grafana/grafana/pkg/services/org" - "github.com/grafana/grafana/pkg/services/contexthandler" + "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" ) diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index 8ee2ad64a19..ac6d530749d 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -24,7 +24,6 @@ import ( "github.com/grafana/grafana/pkg/infra/remotecache" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/login" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/apikey" "github.com/grafana/grafana/pkg/services/apikey/apikeytest" "github.com/grafana/grafana/pkg/services/auth" @@ -33,7 +32,9 @@ import ( "github.com/grafana/grafana/pkg/services/authn/authntest" "github.com/grafana/grafana/pkg/services/contexthandler" "github.com/grafana/grafana/pkg/services/contexthandler/authproxy" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/featuremgmt" + loginsvc "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/login/loginservice" "github.com/grafana/grafana/pkg/services/login/logintest" "github.com/grafana/grafana/pkg/services/navtree" @@ -162,7 +163,7 @@ func TestMiddlewareContext(t *testing.T) { middlewareScenario(t, "middleware should add Cache-Control header for requests with HTML response", func( t *testing.T, sc *scenarioContext) { - sc.handlerFunc = func(c *models.ReqContext) { + sc.handlerFunc = func(c *contextmodel.ReqContext) { t.Log("Handler called") data := &dtos.IndexViewData{ User: &dtos.CurrentUser{}, @@ -436,7 +437,7 @@ func TestMiddlewareContext(t *testing.T) { sc.withTokenSessionCookie("token") sc.userService.ExpectedSignedInUser = &user.SignedInUser{OrgID: 2, UserID: userID} - sc.oauthTokenService.ExpectedAuthUser = &models.UserAuth{UserId: userID, OAuthExpiry: fakeGetTime()().Add(11 * time.Second)} + sc.oauthTokenService.ExpectedAuthUser = &loginsvc.UserAuth{UserId: userID, OAuthExpiry: fakeGetTime()().Add(11 * time.Second)} sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*auth.UserToken, error) { return &auth.UserToken{ @@ -464,7 +465,7 @@ func TestMiddlewareContext(t *testing.T) { sc.withTokenSessionCookie("token") signedInUser := &user.SignedInUser{OrgID: 2, UserID: userID} sc.userService.ExpectedSignedInUser = signedInUser - sc.oauthTokenService.ExpectedAuthUser = &models.UserAuth{ + sc.oauthTokenService.ExpectedAuthUser = &loginsvc.UserAuth{ UserId: userID, OAuthExpiry: fakeGetTime()().Add(-1 * time.Second), OAuthAccessToken: "access_token", @@ -499,7 +500,7 @@ func TestMiddlewareContext(t *testing.T) { sc.withTokenSessionCookie("token") sc.userService.ExpectedSignedInUser = &user.SignedInUser{OrgID: 2, UserID: userID} - sc.oauthTokenService.ExpectedAuthUser = &models.UserAuth{UserId: userID, OAuthExpiry: fakeGetTime()().Add(-5 * time.Second), OAuthRefreshToken: "refreshtoken"} + sc.oauthTokenService.ExpectedAuthUser = &loginsvc.UserAuth{UserId: userID, OAuthExpiry: fakeGetTime()().Add(-5 * time.Second), OAuthRefreshToken: "refreshtoken"} sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*auth.UserToken, error) { return &auth.UserToken{ @@ -526,7 +527,7 @@ func TestMiddlewareContext(t *testing.T) { sc.withTokenSessionCookie("token") sc.userService.ExpectedSignedInUser = &user.SignedInUser{OrgID: 2, UserID: userID} - sc.oauthTokenService.ExpectedAuthUser = &models.UserAuth{UserId: userID} + sc.oauthTokenService.ExpectedAuthUser = &loginsvc.UserAuth{UserId: userID} sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*auth.UserToken, error) { return &auth.UserToken{ @@ -609,7 +610,7 @@ func TestMiddlewareContext(t *testing.T) { middlewareScenario(t, "Should respect auto signup option", func(t *testing.T, sc *scenarioContext) { var actualAuthProxyAutoSignUp *bool = nil - sc.loginService.ExpectedUserFunc = func(cmd *models.UpsertUserCommand) *user.User { + sc.loginService.ExpectedUserFunc = func(cmd *loginsvc.UpsertUserCommand) *user.User { actualAuthProxyAutoSignUp = &cmd.SignupAllowed return nil } @@ -651,7 +652,7 @@ func TestMiddlewareContext(t *testing.T) { middlewareScenario(t, "Should assign role from header to default org", func(t *testing.T, sc *scenarioContext) { var storedRoleInfo map[int64]org.RoleType = nil - sc.loginService.ExpectedUserFunc = func(cmd *models.UpsertUserCommand) *user.User { + sc.loginService.ExpectedUserFunc = func(cmd *loginsvc.UpsertUserCommand) *user.User { storedRoleInfo = cmd.ExternalUser.OrgRoles sc.userService.ExpectedSignedInUser = &user.SignedInUser{OrgID: defaultOrgId, UserID: userID, OrgRole: storedRoleInfo[defaultOrgId]} return &user.User{ID: userID} @@ -674,7 +675,7 @@ func TestMiddlewareContext(t *testing.T) { middlewareScenario(t, "Should NOT assign role from header to non-default org", func(t *testing.T, sc *scenarioContext) { var storedRoleInfo map[int64]org.RoleType = nil - sc.loginService.ExpectedUserFunc = func(cmd *models.UpsertUserCommand) *user.User { + sc.loginService.ExpectedUserFunc = func(cmd *loginsvc.UpsertUserCommand) *user.User { storedRoleInfo = cmd.ExternalUser.OrgRoles sc.userService.ExpectedSignedInUser = &user.SignedInUser{OrgID: orgID, UserID: userID, OrgRole: storedRoleInfo[orgID]} return &user.User{ID: userID} @@ -721,7 +722,7 @@ func TestMiddlewareContext(t *testing.T) { body := "key=value" sc.req.Body = io.NopCloser(strings.NewReader(body)) - sc.handlerFunc = func(c *models.ReqContext) { + sc.handlerFunc = func(c *contextmodel.ReqContext) { t.Log("Handler called") defer func() { err := c.Req.Body.Close() @@ -745,7 +746,7 @@ func TestMiddlewareContext(t *testing.T) { body := "key=value" sc.req.Body = io.NopCloser(strings.NewReader(body)) - sc.handlerFunc = func(c *models.ReqContext) { + sc.handlerFunc = func(c *contextmodel.ReqContext) { t.Log("Handler called") defer func() { err := c.Req.Body.Close() @@ -889,7 +890,7 @@ func middlewareScenario(t *testing.T, desc string, fn scenarioFunc, cbs ...func( sc.jwtAuthService = ctxHdlr.JWTAuthService.(*jwt.FakeJWTService) sc.remoteCacheService = ctxHdlr.RemoteCache - sc.defaultHandler = func(c *models.ReqContext) { + sc.defaultHandler = func(c *contextmodel.ReqContext) { require.NotNil(t, c) t.Log("Default HTTP handler called") sc.context = c diff --git a/pkg/middleware/quota.go b/pkg/middleware/quota.go index 7a0689ff11d..31568e7fd5a 100644 --- a/pkg/middleware/quota.go +++ b/pkg/middleware/quota.go @@ -3,7 +3,7 @@ package middleware import ( "fmt" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/web" ) @@ -15,7 +15,7 @@ func Quota(quotaService quota.Service) func(string) web.Handler { } //https://open.spotify.com/track/7bZSoBEAEEUsGEuLOf94Jm?si=T1Tdju5qRSmmR0zph_6RBw fuuuuunky return func(targetSrv string) web.Handler { - return func(c *models.ReqContext) { + return func(c *contextmodel.ReqContext) { limitReached, err := quotaService.QuotaReached(c, quota.TargetSrv(targetSrv)) if err != nil { c.JsonApiErr(500, "Failed to get quota", err) diff --git a/pkg/middleware/recovery_test.go b/pkg/middleware/recovery_test.go index 1a8fe8537c5..8a4251c7917 100644 --- a/pkg/middleware/recovery_test.go +++ b/pkg/middleware/recovery_test.go @@ -9,8 +9,8 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/remotecache" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/auth/authtest" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web" ) @@ -42,7 +42,7 @@ func TestRecoveryMiddleware(t *testing.T) { }) } -func panicHandler(c *models.ReqContext) { +func panicHandler(c *contextmodel.ReqContext) { panic("Handler has panicked") } @@ -73,7 +73,7 @@ func recoveryScenario(t *testing.T, desc string, url string, fn scenarioFunc) { // mock out gc goroutine sc.m.Use(OrgRedirect(cfg, sc.userService)) - sc.defaultHandler = func(c *models.ReqContext) { + sc.defaultHandler = func(c *contextmodel.ReqContext) { sc.context = c if sc.handlerFunc != nil { sc.handlerFunc(sc.context) diff --git a/pkg/middleware/request_metrics.go b/pkg/middleware/request_metrics.go index 9706968fd2e..b37e49ebc36 100644 --- a/pkg/middleware/request_metrics.go +++ b/pkg/middleware/request_metrics.go @@ -6,12 +6,13 @@ import ( "strings" "time" + "github.com/prometheus/client_golang/prometheus" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/web" - "github.com/prometheus/client_golang/prometheus" ) var ( diff --git a/pkg/middleware/testing.go b/pkg/middleware/testing.go index 2c27fcf7ae2..6caa397c4e8 100644 --- a/pkg/middleware/testing.go +++ b/pkg/middleware/testing.go @@ -11,12 +11,12 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/db/dbtest" "github.com/grafana/grafana/pkg/infra/remotecache" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/apikey/apikeytest" "github.com/grafana/grafana/pkg/services/auth/authtest" "github.com/grafana/grafana/pkg/services/auth/jwt" "github.com/grafana/grafana/pkg/services/contexthandler" "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/login/loginservice" "github.com/grafana/grafana/pkg/services/org/orgtest" "github.com/grafana/grafana/pkg/services/user/usertest" @@ -27,7 +27,7 @@ import ( type scenarioContext struct { t *testing.T m *web.Mux - context *models.ReqContext + context *contextmodel.ReqContext resp *httptest.ResponseRecorder apiKey string authHeader string @@ -80,7 +80,7 @@ func (sc *scenarioContext) fakeReq(method, url string) *scenarioContext { req, err := http.NewRequest(method, url, nil) require.NoError(sc.t, err) - reqCtx := &models.ReqContext{ + reqCtx := &contextmodel.ReqContext{ Context: web.FromContext(req.Context()), } sc.req = req.WithContext(ctxkey.Set(req.Context(), reqCtx)) @@ -102,7 +102,7 @@ func (sc *scenarioContext) fakeReqWithParams(method, url string, queryParams map req.URL.RawQuery = q.Encode() require.NoError(sc.t, err) - reqCtx := &models.ReqContext{ + reqCtx := &contextmodel.ReqContext{ Context: web.FromContext(req.Context()), } sc.req = req.WithContext(ctxkey.Set(req.Context(), reqCtx)) @@ -147,4 +147,4 @@ func (sc *scenarioContext) exec() { } type scenarioFunc func(t *testing.T, c *scenarioContext) -type handlerFunc func(c *models.ReqContext) +type handlerFunc func(c *contextmodel.ReqContext) diff --git a/pkg/middleware/validate_host.go b/pkg/middleware/validate_host.go index bf9d2db9c3d..3b75552cd58 100644 --- a/pkg/middleware/validate_host.go +++ b/pkg/middleware/validate_host.go @@ -3,13 +3,13 @@ package middleware import ( "strings" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web" ) func ValidateHostHeader(cfg *setting.Cfg) web.Handler { - return func(c *models.ReqContext) { + return func(c *contextmodel.ReqContext) { // ignore local render calls if c.IsRenderCall { return diff --git a/pkg/mocks/mock_gcsifaces/mocks.go b/pkg/mocks/mock_gcsifaces/mocks.go index 68fda5383d1..2a10e18b375 100644 --- a/pkg/mocks/mock_gcsifaces/mocks.go +++ b/pkg/mocks/mock_gcsifaces/mocks.go @@ -10,9 +10,10 @@ import ( storage "cloud.google.com/go/storage" gomock "github.com/golang/mock/gomock" - gcsifaces "github.com/grafana/grafana/pkg/ifaces/gcsifaces" google "golang.org/x/oauth2/google" jwt "golang.org/x/oauth2/jwt" + + gcsifaces "github.com/grafana/grafana/pkg/ifaces/gcsifaces" ) // MockStorageClient is a mock of StorageClient interface diff --git a/pkg/models/user_auth.go b/pkg/models/user_auth.go deleted file mode 100644 index ec1f70e75a0..00000000000 --- a/pkg/models/user_auth.go +++ /dev/null @@ -1,136 +0,0 @@ -package models - -import ( - "fmt" - "time" - - "github.com/grafana/grafana/pkg/services/org" - "github.com/grafana/grafana/pkg/services/user" - "github.com/grafana/grafana/pkg/setting" - - "golang.org/x/oauth2" -) - -type UserAuth struct { - Id int64 - UserId int64 - AuthModule string - AuthId string - Created time.Time - OAuthAccessToken string - OAuthRefreshToken string - OAuthIdToken string - OAuthTokenType string - OAuthExpiry time.Time -} - -type ExternalUserInfo struct { - OAuthToken *oauth2.Token - AuthModule string - AuthId string - UserId int64 - Email string - Login string - Name string - Groups []string - OrgRoles map[int64]org.RoleType - IsGrafanaAdmin *bool // This is a pointer to know if we should sync this or not (nil = ignore sync) - IsDisabled bool -} - -func (e *ExternalUserInfo) String() string { - return fmt.Sprintf("%+v", *e) -} - -type LoginInfo struct { - AuthModule string - User *user.User - ExternalUser ExternalUserInfo - LoginUsername string - HTTPStatus int - Error error -} - -// RequestURIKey is used as key to save request URI in contexts -// (used for the Enterprise auditing feature) -type RequestURIKey struct{} - -// --------------------- -// COMMANDS - -type UpsertUserCommand struct { - ReqContext *ReqContext - ExternalUser *ExternalUserInfo - UserLookupParams - SignupAllowed bool - - Result *user.User -} - -type SetAuthInfoCommand struct { - AuthModule string - AuthId string - UserId int64 - OAuthToken *oauth2.Token -} - -type UpdateAuthInfoCommand struct { - AuthModule string - AuthId string - UserId int64 - OAuthToken *oauth2.Token -} - -type DeleteAuthInfoCommand struct { - UserAuth *UserAuth -} - -// ---------------------- -// QUERIES - -type LoginUserQuery struct { - ReqContext *ReqContext - Username string - Password string - User *user.User - IpAddress string - AuthModule string - Cfg *setting.Cfg -} - -type GetUserByAuthInfoQuery struct { - AuthModule string - AuthId string - UserLookupParams -} - -type UserLookupParams struct { - // Describes lookup order as well - UserID *int64 // if set, will try to find the user by id - Email *string // if set, will try to find the user by email - Login *string // if set, will try to find the user by login -} - -type GetExternalUserInfoByLoginQuery struct { - LoginOrEmail string - - Result *ExternalUserInfo -} - -type GetAuthInfoQuery struct { - UserId int64 - AuthModule string - AuthId string - - Result *UserAuth -} - -type GetUserLabelsQuery struct { - UserIDs []int64 -} - -type TeamOrgGroupDTO struct { - TeamName string `json:"teamName"` - OrgName string `json:"orgName"` - GroupDN string `json:"groupDN"` -} diff --git a/pkg/plugins/accesscontrol.go b/pkg/plugins/accesscontrol.go index fef15b2404b..cc8c1b59e65 100644 --- a/pkg/plugins/accesscontrol.go +++ b/pkg/plugins/accesscontrol.go @@ -1,8 +1,8 @@ package plugins import ( - "github.com/grafana/grafana/pkg/models" ac "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/setting" ) @@ -22,9 +22,9 @@ var ( AdminAccessEvaluator = ac.EvalAny(ac.EvalPermission(ActionWrite), ac.EvalPermission(ActionInstall)) ) -func ReqCanAdminPlugins(cfg *setting.Cfg) func(rc *models.ReqContext) bool { +func ReqCanAdminPlugins(cfg *setting.Cfg) func(rc *contextmodel.ReqContext) bool { // Legacy handler that protects access to the Configuration > Plugins page - return func(rc *models.ReqContext) bool { + return func(rc *contextmodel.ReqContext) bool { return rc.OrgRole == org.RoleAdmin || cfg.PluginAdminEnabled && rc.IsGrafanaAdmin } } diff --git a/pkg/plugins/config/config.go b/pkg/plugins/config/config.go index 1c97e241819..c73b71e3b5d 100644 --- a/pkg/plugins/config/config.go +++ b/pkg/plugins/config/config.go @@ -30,6 +30,8 @@ type Cfg struct { BuildVersion string // TODO Remove LogDatasourceRequests bool + + PluginsCDNURLTemplate string } func ProvideConfig(settingProvider setting.Provider, grafanaCfg *setting.Cfg) *Cfg { @@ -63,6 +65,7 @@ func NewCfg(settingProvider setting.Provider, grafanaCfg *setting.Cfg) *Cfg { AWSAssumeRoleEnabled: aws.KeyValue("assume_role_enabled").MustBool(grafanaCfg.AWSAssumeRoleEnabled), Azure: grafanaCfg.Azure, LogDatasourceRequests: grafanaCfg.IsFeatureToggleEnabled(featuremgmt.FlagDatasourceLogger), + PluginsCDNURLTemplate: grafanaCfg.PluginsCDNURLTemplate, } } diff --git a/pkg/plugins/manager/client/clienttest/clienttest.go b/pkg/plugins/manager/client/clienttest/clienttest.go index f88d07d3376..64dcd0a78e1 100644 --- a/pkg/plugins/manager/client/clienttest/clienttest.go +++ b/pkg/plugins/manager/client/clienttest/clienttest.go @@ -7,10 +7,10 @@ import ( "testing" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/manager/client" "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/web" "github.com/stretchr/testify/require" @@ -130,7 +130,7 @@ type ClientDecoratorTest struct { TestClient *TestClient Middlewares []plugins.ClientMiddleware Decorator *client.Decorator - ReqContext *models.ReqContext + ReqContext *contextmodel.ReqContext QueryDataReq *backend.QueryDataRequest QueryDataCtx context.Context CallResourceReq *backend.CallResourceRequest @@ -181,7 +181,7 @@ func NewClientDecoratorTest(t *testing.T, opts ...ClientDecoratorTestOption) *Cl func WithReqContext(req *http.Request, user *user.SignedInUser) ClientDecoratorTestOption { return ClientDecoratorTestOption(func(cdt *ClientDecoratorTest) { if cdt.ReqContext == nil { - cdt.ReqContext = &models.ReqContext{ + cdt.ReqContext = &contextmodel.ReqContext{ Context: &web.Context{}, SignedInUser: user, } diff --git a/pkg/plugins/manager/loader/assetpath/assetpath.go b/pkg/plugins/manager/loader/assetpath/assetpath.go new file mode 100644 index 00000000000..57ebc7557da --- /dev/null +++ b/pkg/plugins/manager/loader/assetpath/assetpath.go @@ -0,0 +1,70 @@ +package assetpath + +import ( + "fmt" + "net/url" + "path" + "path/filepath" + "strings" + + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/pluginscdn" +) + +// Service provides methods for constructing asset paths for plugins. +// It supports core plugins, external plugins stored on the local filesystem, and external plugins stored +// on the plugins CDN, and it will switch to the correct implementation depending on the plugin and the config. +type Service struct { + cdn *pluginscdn.Service +} + +func ProvideService(cdn *pluginscdn.Service) *Service { + return &Service{cdn: cdn} +} + +// Base returns the base path for the specified plugin. +func (s *Service) Base(pluginJSON plugins.JSONData, class plugins.Class, pluginDir string) (string, error) { + if class == plugins.Core { + return path.Join("public/app/plugins", string(pluginJSON.Type), filepath.Base(pluginDir)), nil + } + if s.cdn.PluginSupported(pluginJSON.ID) { + return s.cdn.SystemJSAssetPath(pluginJSON.ID, pluginJSON.Info.Version, "") + } + return path.Join("public/plugins", pluginJSON.ID), nil +} + +// Module returns the module.js path for the specified plugin. +func (s *Service) Module(pluginJSON plugins.JSONData, class plugins.Class, pluginDir string) (string, error) { + if class == plugins.Core { + return path.Join("app/plugins", string(pluginJSON.Type), filepath.Base(pluginDir), "module"), nil + } + if s.cdn.PluginSupported(pluginJSON.ID) { + return s.cdn.SystemJSAssetPath(pluginJSON.ID, pluginJSON.Info.Version, "module") + } + return path.Join("plugins", pluginJSON.ID, "module"), nil +} + +// RelativeURL returns the relative URL for an arbitrary plugin asset. +// If pathStr is an empty string, defaultStr is returned. +func (s *Service) RelativeURL(p *plugins.Plugin, pathStr, defaultStr string) (string, error) { + if pathStr == "" { + return defaultStr, nil + } + if s.cdn.PluginSupported(p.ID) { + // CDN + return s.cdn.NewCDNURLConstructor(p.ID, p.Info.Version).StringPath(pathStr) + } + // Local + u, err := url.Parse(pathStr) + if err != nil { + return "", fmt.Errorf("url parse: %w", err) + } + if u.IsAbs() { + return pathStr, nil + } + // is set as default or has already been prefixed with base path + if pathStr == defaultStr || strings.HasPrefix(pathStr, p.BaseURL) { + return pathStr, nil + } + return path.Join(p.BaseURL, pathStr), nil +} diff --git a/pkg/plugins/manager/loader/assetpath/assetpath_test.go b/pkg/plugins/manager/loader/assetpath/assetpath_test.go new file mode 100644 index 00000000000..1551aa57146 --- /dev/null +++ b/pkg/plugins/manager/loader/assetpath/assetpath_test.go @@ -0,0 +1,88 @@ +package assetpath + +import ( + "testing" + + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/config" + "github.com/grafana/grafana/pkg/plugins/pluginscdn" + "github.com/stretchr/testify/require" +) + +func extPath(pluginID string) string { + return "/grafana/data/plugins/" + pluginID +} + +func TestService(t *testing.T) { + svc := ProvideService(pluginscdn.ProvideService(&config.Cfg{ + PluginsCDNURLTemplate: "https://cdn.example.com/{id}/{version}/public/plugins/{id}/{assetPath}", + PluginSettings: map[string]map[string]string{ + "one": {"cdn": "true"}, + "two": {}, + }, + })) + + const tableOldPath = "/grafana/public/app/plugins/panel/table-old" + jsonData := map[string]plugins.JSONData{ + "table-old": {ID: "table-old", Info: plugins.Info{Version: "1.0.0"}}, + + "one": {ID: "one", Info: plugins.Info{Version: "1.0.0"}}, + "two": {ID: "two", Info: plugins.Info{Version: "2.0.0"}}, + } + + t.Run("Base", func(t *testing.T) { + base, err := svc.Base(jsonData["one"], plugins.External, extPath("one")) + require.NoError(t, err) + require.Equal(t, "plugin-cdn/one/1.0.0/public/plugins/one", base) + + base, err = svc.Base(jsonData["two"], plugins.External, extPath("two")) + require.NoError(t, err) + require.Equal(t, "public/plugins/two", base) + + base, err = svc.Base(jsonData["table-old"], plugins.Core, tableOldPath) + require.NoError(t, err) + require.Equal(t, "public/app/plugins/table-old", base) + }) + + t.Run("Module", func(t *testing.T) { + module, err := svc.Module(jsonData["one"], plugins.External, extPath("one")) + require.NoError(t, err) + require.Equal(t, "plugin-cdn/one/1.0.0/public/plugins/one/module", module) + + module, err = svc.Module(jsonData["two"], plugins.External, extPath("two")) + require.NoError(t, err) + require.Equal(t, "plugins/two/module", module) + + module, err = svc.Module(jsonData["table-old"], plugins.Core, tableOldPath) + require.NoError(t, err) + require.Equal(t, "app/plugins/table-old/module", module) + }) + + t.Run("RelativeURL", func(t *testing.T) { + pluginsMap := map[string]*plugins.Plugin{ + "one": { + JSONData: plugins.JSONData{ID: "one", Info: plugins.Info{Version: "1.0.0"}}, + BaseURL: "plugin-cdn/one/1.0.0/public/pluginsMap/one", + }, + "two": { + JSONData: plugins.JSONData{ID: "two", Info: plugins.Info{Version: "2.0.0"}}, + BaseURL: "public/pluginsMap/two", + }, + } + u, err := svc.RelativeURL(pluginsMap["one"], "", "default") + require.NoError(t, err) + require.Equal(t, "default", u) + + u, err = svc.RelativeURL(pluginsMap["one"], "path/to/file.txt", "default") + require.NoError(t, err) + require.Equal(t, "https://cdn.example.com/one/1.0.0/public/plugins/one/path/to/file.txt", u) + + u, err = svc.RelativeURL(pluginsMap["two"], "path/to/file.txt", "default") + require.NoError(t, err) + require.Equal(t, "public/pluginsMap/two/path/to/file.txt", u) + + u, err = svc.RelativeURL(pluginsMap["two"], "default", "default") + require.NoError(t, err) + require.Equal(t, "default", u) + }) +} diff --git a/pkg/plugins/manager/loader/loader.go b/pkg/plugins/manager/loader/loader.go index 9c2abac8d8b..1d7d99bcbea 100644 --- a/pkg/plugins/manager/loader/loader.go +++ b/pkg/plugins/manager/loader/loader.go @@ -5,7 +5,6 @@ import ( "encoding/json" "errors" "fmt" - "net/url" "os" "path" "path/filepath" @@ -19,11 +18,13 @@ import ( "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/logger" + "github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" "github.com/grafana/grafana/pkg/plugins/manager/loader/finder" "github.com/grafana/grafana/pkg/plugins/manager/loader/initializer" "github.com/grafana/grafana/pkg/plugins/manager/process" "github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/manager/signature" + "github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/plugins/storage" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/util" @@ -44,21 +45,25 @@ type Loader struct { pluginInitializer initializer.Initializer signatureValidator signature.Validator pluginStorage storage.Manager + pluginsCDN *pluginscdn.Service + assetPath *assetpath.Service log log.Logger + cfg *config.Cfg errs map[string]*plugins.SignatureError } func ProvideService(cfg *config.Cfg, license plugins.Licensing, authorizer plugins.PluginLoaderAuthorizer, pluginRegistry registry.Service, backendProvider plugins.BackendFactoryProvider, - roleRegistry plugins.RoleRegistry) *Loader { + roleRegistry plugins.RoleRegistry, pluginsCDNService *pluginscdn.Service, assetPath *assetpath.Service) *Loader { return New(cfg, license, authorizer, pluginRegistry, backendProvider, process.NewManager(pluginRegistry), - storage.FileSystem(logger.NewLogger("loader.fs"), cfg.PluginsPath), roleRegistry) + storage.FileSystem(logger.NewLogger("loader.fs"), cfg.PluginsPath), roleRegistry, pluginsCDNService, assetPath) } func New(cfg *config.Cfg, license plugins.Licensing, authorizer plugins.PluginLoaderAuthorizer, pluginRegistry registry.Service, backendProvider plugins.BackendFactoryProvider, - processManager process.Service, pluginStorage storage.Manager, roleRegistry plugins.RoleRegistry) *Loader { + processManager process.Service, pluginStorage storage.Manager, roleRegistry plugins.RoleRegistry, + pluginsCDNService *pluginscdn.Service, assetPath *assetpath.Service) *Loader { return &Loader{ pluginFinder: finder.New(), pluginRegistry: pluginRegistry, @@ -69,6 +74,9 @@ func New(cfg *config.Cfg, license plugins.Licensing, authorizer plugins.PluginLo errs: make(map[string]*plugins.SignatureError), log: log.New("plugin.loader"), roleRegistry: roleRegistry, + cfg: cfg, + pluginsCDN: pluginsCDNService, + assetPath: assetPath, } } @@ -81,6 +89,36 @@ func (l *Loader) Load(ctx context.Context, class plugins.Class, paths []string) return l.loadPlugins(ctx, class, pluginJSONPaths) } +func (l *Loader) createPluginsForLoading(class plugins.Class, foundPlugins foundPlugins) map[string]*plugins.Plugin { + loadedPlugins := make(map[string]*plugins.Plugin) + for pluginDir, pluginJSON := range foundPlugins { + plugin, err := l.createPluginBase(pluginJSON, class, pluginDir) + if err != nil { + l.log.Warn("Could not create plugin base", "pluginID", pluginJSON.ID, "err", err) + continue + } + + // calculate initial signature state + var sig plugins.Signature + if l.pluginsCDN.PluginSupported(plugin.ID) { + // CDN plugins have no signature checks for now. + sig = plugins.Signature{Status: plugins.SignatureValid} + } else { + sig, err = signature.Calculate(l.log, plugin) + if err != nil { + l.log.Warn("Could not calculate plugin signature state", "pluginID", plugin.ID, "err", err) + continue + } + } + plugin.Signature = sig.Status + plugin.SignatureType = sig.Type + plugin.SignatureOrg = sig.SigningOrg + + loadedPlugins[plugin.PluginDir] = plugin + } + return loadedPlugins +} + func (l *Loader) loadPlugins(ctx context.Context, class plugins.Class, pluginJSONPaths []string) ([]*plugins.Plugin, error) { var foundPlugins = foundPlugins{} @@ -113,22 +151,8 @@ func (l *Loader) loadPlugins(ctx context.Context, class plugins.Class, pluginJSO foundPlugins.stripDuplicates(registeredPlugins, l.log) - // calculate initial signature state - loadedPlugins := make(map[string]*plugins.Plugin) - for pluginDir, pluginJSON := range foundPlugins { - plugin := createPluginBase(pluginJSON, class, pluginDir) - - sig, err := signature.Calculate(l.log, plugin) - if err != nil { - l.log.Warn("Could not calculate plugin signature state", "pluginID", plugin.ID, "err", err) - continue - } - plugin.Signature = sig.Status - plugin.SignatureType = sig.Type - plugin.SignatureOrg = sig.SigningOrg - - loadedPlugins[plugin.PluginDir] = plugin - } + // create plugins structs and calculate signatures + loadedPlugins := l.createPluginsForLoading(class, foundPlugins) // wire up plugin dependencies for _, plugin := range loadedPlugins { @@ -165,12 +189,13 @@ func (l *Loader) loadPlugins(ctx context.Context, class plugins.Class, pluginJSO // clear plugin error if a pre-existing error has since been resolved delete(l.errs, plugin.ID) - // verify module.js exists for SystemJS to load + // verify module.js exists for SystemJS to load. + // CDN plugins can be loaded with plugin.json only, so do not warn for those. if !plugin.IsRenderer() && !plugin.IsCorePlugin() { module := filepath.Join(plugin.PluginDir, "module.js") if exists, err := fs.Exists(module); err != nil { return nil, err - } else if !exists { + } else if !exists && !l.pluginsCDN.PluginSupported(plugin.ID) { l.log.Warn("Plugin missing module.js", "pluginID", plugin.ID, "warning", "Missing module.js, If you loaded this plugin from git, make sure to compile it.", @@ -312,28 +337,47 @@ func (l *Loader) readPluginJSON(pluginJSONPath string) (plugins.JSONData, error) return plugin, nil } -func createPluginBase(pluginJSON plugins.JSONData, class plugins.Class, pluginDir string) *plugins.Plugin { +func (l *Loader) createPluginBase(pluginJSON plugins.JSONData, class plugins.Class, pluginDir string) (*plugins.Plugin, error) { + baseURL, err := l.assetPath.Base(pluginJSON, class, pluginDir) + if err != nil { + return nil, fmt.Errorf("base url: %w", err) + } + moduleURL, err := l.assetPath.Module(pluginJSON, class, pluginDir) + if err != nil { + return nil, fmt.Errorf("module url: %w", err) + } plugin := &plugins.Plugin{ JSONData: pluginJSON, PluginDir: pluginDir, - BaseURL: baseURL(pluginJSON, class, pluginDir), - Module: module(pluginJSON, class, pluginDir), + BaseURL: baseURL, + Module: moduleURL, Class: class, } plugin.SetLogger(log.New(fmt.Sprintf("plugin.%s", plugin.ID))) - setImages(plugin) + if err := l.setImages(plugin); err != nil { + return nil, err + } - return plugin + return plugin, nil } -func setImages(p *plugins.Plugin) { - p.Info.Logos.Small = pluginLogoURL(p.Type, p.Info.Logos.Small, p.BaseURL) - p.Info.Logos.Large = pluginLogoURL(p.Type, p.Info.Logos.Large, p.BaseURL) - - for i := 0; i < len(p.Info.Screenshots); i++ { - p.Info.Screenshots[i].Path = evalRelativePluginURLPath(p.Info.Screenshots[i].Path, p.BaseURL, p.Type) +func (l *Loader) setImages(p *plugins.Plugin) error { + var err error + for _, dst := range []*string{&p.Info.Logos.Small, &p.Info.Logos.Large} { + *dst, err = l.assetPath.RelativeURL(p, *dst, defaultLogoPath(p.Type)) + if err != nil { + return fmt.Errorf("logo: %w", err) + } } + for i := 0; i < len(p.Info.Screenshots); i++ { + screenshot := &p.Info.Screenshots[i] + screenshot.Path, err = l.assetPath.RelativeURL(p, screenshot.Path, "") + if err != nil { + return fmt.Errorf("screenshot %d relative url: %w", i, err) + } + } + return nil } func setDefaultNavURL(p *plugins.Plugin) { @@ -377,36 +421,10 @@ func configureAppChildPlugin(parent *plugins.Plugin, child *plugins.Plugin) { } } -func pluginLogoURL(pluginType plugins.Type, path, baseURL string) string { - if path == "" { - return defaultLogoPath(pluginType) - } - - return evalRelativePluginURLPath(path, baseURL, pluginType) -} - func defaultLogoPath(pluginType plugins.Type) string { return "public/img/icn-" + string(pluginType) + ".svg" } -func evalRelativePluginURLPath(pathStr, baseURL string, pluginType plugins.Type) string { - if pathStr == "" { - return "" - } - - u, _ := url.Parse(pathStr) - if u.IsAbs() { - return pathStr - } - - // is set as default or has already been prefixed with base path - if pathStr == defaultLogoPath(pluginType) || strings.HasPrefix(pathStr, baseURL) { - return pathStr - } - - return path.Join(baseURL, pathStr) -} - func (l *Loader) PluginErrors() []*plugins.Error { errs := make([]*plugins.Error, 0) for _, err := range l.errs { @@ -419,20 +437,6 @@ func (l *Loader) PluginErrors() []*plugins.Error { return errs } -func baseURL(pluginJSON plugins.JSONData, class plugins.Class, pluginDir string) string { - if class == plugins.Core { - return path.Join("public/app/plugins", string(pluginJSON.Type), filepath.Base(pluginDir)) - } - return path.Join("public/plugins", pluginJSON.ID) -} - -func module(pluginJSON plugins.JSONData, class plugins.Class, pluginDir string) string { - if class == plugins.Core { - return path.Join("app/plugins", string(pluginJSON.Type), filepath.Base(pluginDir), "module") - } - return path.Join("plugins", pluginJSON.ID, "module") -} - func validatePluginJSON(data plugins.JSONData) error { if data.ID == "" || !data.Type.IsValid() { return ErrInvalidPluginJSON diff --git a/pkg/plugins/manager/loader/loader_test.go b/pkg/plugins/manager/loader/loader_test.go index 4c0ccbbcd6c..190ae69a90e 100644 --- a/pkg/plugins/manager/loader/loader_test.go +++ b/pkg/plugins/manager/loader/loader_test.go @@ -7,6 +7,9 @@ import ( "sort" "testing" + "github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" + "github.com/grafana/grafana/pkg/plugins/pluginscdn" + "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/stretchr/testify/require" @@ -404,6 +407,61 @@ func TestLoader_Load(t *testing.T) { }, }, }, + { + name: "Load CDN plugin", + class: plugins.External, + cfg: &config.Cfg{ + PluginsCDNURLTemplate: "https://cdn.example.com/{id}/{version}/public/plugins/{id}/{assetPath}", + PluginSettings: setting.PluginSettings{ + "grafana-worldmap-panel": {"cdn": "true"}, + }, + }, + pluginPaths: []string{"../testdata/cdn"}, + want: []*plugins.Plugin{ + { + JSONData: plugins.JSONData{ + ID: "grafana-worldmap-panel", + Type: "panel", + Name: "Worldmap Panel", + Info: plugins.Info{ + Version: "0.3.3", + Links: []plugins.InfoLink{ + {Name: "Project site", URL: "https://github.com/grafana/worldmap-panel"}, + {Name: "MIT License", URL: "https://github.com/grafana/worldmap-panel/blob/master/LICENSE"}, + }, + Logos: plugins.Logos{ + // Path substitution + Small: "https://cdn.example.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/images/worldmap_logo.svg", + Large: "https://cdn.example.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/images/worldmap_logo.svg", + }, + Screenshots: []plugins.Screenshots{ + { + Name: "World", + Path: "https://cdn.example.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/images/worldmap-world.png", + }, + { + Name: "USA", + Path: "https://cdn.example.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/images/worldmap-usa.png", + }, + { + Name: "Light Theme", + Path: "https://cdn.example.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/images/worldmap-light-theme.png", + }, + }, + }, + Dependencies: plugins.Dependencies{ + GrafanaVersion: "3.x.x", + Plugins: []plugins.Dependency{}, + }, + }, + PluginDir: filepath.Join(parentDir, "testdata/cdn/plugin"), + Class: plugins.External, + Signature: plugins.SignatureValid, + BaseURL: "plugin-cdn/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel", + Module: "plugin-cdn/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/module", + }, + }, + }, } for _, tt := range tests { reg := fakes.NewFakePluginRegistry() @@ -1320,9 +1378,10 @@ func Test_setPathsBasedOnApp(t *testing.T) { } func newLoader(cfg *config.Cfg, cbs ...func(loader *Loader)) *Loader { + cdn := pluginscdn.ProvideService(cfg) l := New(cfg, &fakes.FakeLicensingService{}, signature.NewUnsignedAuthorizer(cfg), fakes.NewFakePluginRegistry(), fakes.NewFakeBackendProcessProvider(), fakes.NewFakeProcessManager(), fakes.NewFakePluginStorage(), - fakes.NewFakeRoleRegistry()) + fakes.NewFakeRoleRegistry(), cdn, assetpath.ProvideService(cdn)) for _, cb := range cbs { cb(l) diff --git a/pkg/plugins/manager/manager_integration_test.go b/pkg/plugins/manager/manager_integration_test.go index fe1391c862d..5fcc288e917 100644 --- a/pkg/plugins/manager/manager_integration_test.go +++ b/pkg/plugins/manager/manager_integration_test.go @@ -8,6 +8,9 @@ import ( "testing" "time" + "github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" + "github.com/grafana/grafana/pkg/plugins/pluginscdn" + "github.com/grafana/grafana-azure-sdk-go/azsettings" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" @@ -110,10 +113,12 @@ func TestIntegrationPluginManager(t *testing.T) { pCfg := config.ProvideConfig(setting.ProvideProvider(cfg), cfg) reg := registry.ProvideService() + cdn := pluginscdn.ProvideService(pCfg) lic := plicensing.ProvideLicensing(cfg, &licensing.OSSLicensingService{Cfg: cfg}) l := loader.ProvideService(pCfg, lic, signature.NewUnsignedAuthorizer(pCfg), - reg, provider.ProvideService(coreRegistry), fakes.NewFakeRoleRegistry()) + reg, provider.ProvideService(coreRegistry), fakes.NewFakeRoleRegistry(), + cdn, assetpath.ProvideService(cdn)) ps, err := store.ProvideService(cfg, pCfg, reg, l) require.NoError(t, err) diff --git a/pkg/plugins/manager/signature/signature.go b/pkg/plugins/manager/signature/signature.go index 2e7262d7569..3954a4850d6 100644 --- a/pkg/plugins/manager/signature/signature.go +++ b/pkg/plugins/manager/signature/signature.go @@ -18,14 +18,14 @@ func NewValidator(authorizer plugins.PluginLoaderAuthorizer) Validator { } func (s *Validator) Validate(plugin *plugins.Plugin) *plugins.SignatureError { - if plugin.Signature == plugins.SignatureValid { + if plugin.Signature.IsValid() { s.log.Debug("Plugin has valid signature", "id", plugin.ID) return nil } // If a plugin is nested within another, create links to each other to inherit signature details if plugin.Parent != nil { - if plugin.IsCorePlugin() || plugin.Signature == plugins.SignatureInternal { + if plugin.IsCorePlugin() || plugin.Signature.IsInternal() { s.log.Debug("Not setting descendant plugin's signature to that of root since it's core or internal", "plugin", plugin.ID, "signature", plugin.Signature, "isCore", plugin.IsCorePlugin()) } else { @@ -34,7 +34,7 @@ func (s *Validator) Validate(plugin *plugins.Plugin) *plugins.SignatureError { plugin.Signature = plugin.Parent.Signature plugin.SignatureType = plugin.Parent.SignatureType plugin.SignatureOrg = plugin.Parent.SignatureOrg - if plugin.Signature == plugins.SignatureValid { + if plugin.Signature.IsValid() { s.log.Debug("Plugin has valid signature (inherited from root)", "id", plugin.ID) return nil } diff --git a/pkg/plugins/manager/testdata/cdn/plugin/plugin.json b/pkg/plugins/manager/testdata/cdn/plugin/plugin.json new file mode 100644 index 00000000000..e0c88b874da --- /dev/null +++ b/pkg/plugins/manager/testdata/cdn/plugin/plugin.json @@ -0,0 +1,40 @@ +{ + "type": "panel", + "name": "Worldmap Panel", + "id": "grafana-worldmap-panel", + "info": { + "logos": { + "small": "images/worldmap_logo.svg", + "large": "images/worldmap_logo.svg" + }, + "links": [ + { + "name": "Project site", + "url": "https://github.com/grafana/worldmap-panel" + }, + { + "name": "MIT License", + "url": "https://github.com/grafana/worldmap-panel/blob/master/LICENSE" + } + ], + "screenshots": [ + { + "name": "World", + "path": "images/worldmap-world.png" + }, + { + "name": "USA", + "path": "images/worldmap-usa.png" + }, + { + "name": "Light Theme", + "path": "images/worldmap-light-theme.png" + } + ], + "version": "0.3.3" + }, + "dependencies": { + "grafanaVersion": "3.x.x", + "plugins": [] + } +} diff --git a/pkg/plugins/pfs/pfs_test.go b/pkg/plugins/pfs/pfs_test.go index 7f1dd59ecb2..bef758a5924 100644 --- a/pkg/plugins/pfs/pfs_test.go +++ b/pkg/plugins/pfs/pfs_test.go @@ -127,6 +127,10 @@ func TestParsePluginTestdata(t *testing.T) { "disallowed-cue-import": { err: ErrDisallowedCUEImport, }, + "cdn": { + rootid: "grafana-worldmap-panel", + subpath: "plugin", + }, } staticRootPath, err := filepath.Abs("../manager/testdata") diff --git a/pkg/plugins/pluginscdn/pluginscdn.go b/pkg/plugins/pluginscdn/pluginscdn.go new file mode 100644 index 00000000000..3539d9ac765 --- /dev/null +++ b/pkg/plugins/pluginscdn/pluginscdn.go @@ -0,0 +1,80 @@ +package pluginscdn + +import ( + "errors" + "fmt" + "net/url" + "path" + + "github.com/grafana/grafana/pkg/plugins/config" +) + +const ( + // systemJSCDNKeyword is the path prefix used by system.js to identify the plugins CDN. + systemJSCDNKeyword = "plugin-cdn" +) + +var ErrPluginNotCDN = errors.New("plugin is not a cdn plugin") + +// Service provides methods for the plugins CDN. +type Service struct { + cfg *config.Cfg +} + +func ProvideService(cfg *config.Cfg) *Service { + return &Service{cfg: cfg} +} + +// NewCDNURLConstructor returns a new URLConstructor for the provided plugin id and version. +// The CDN should be enabled for the plugin, otherwise the returned URLConstructor will have +// and invalid base url. +func (s *Service) NewCDNURLConstructor(pluginID, pluginVersion string) URLConstructor { + return URLConstructor{ + cdnURLTemplate: s.cfg.PluginsCDNURLTemplate, + pluginID: pluginID, + pluginVersion: pluginVersion, + } +} + +// IsEnabled returns true if the plugins cdn is enabled. +func (s *Service) IsEnabled() bool { + return s.cfg.PluginsCDNURLTemplate != "" +} + +// PluginSupported returns true if the CDN is enabled in the config and if the specified plugin ID has CDN enabled. +func (s *Service) PluginSupported(pluginID string) bool { + return s.IsEnabled() && s.cfg.PluginSettings[pluginID]["cdn"] != "" +} + +// BaseURL returns the absolute base URL of the plugins CDN. +// If the plugins CDN is disabled, it returns an empty string. +func (s *Service) BaseURL() (string, error) { + if !s.IsEnabled() { + return "", nil + } + u, err := url.Parse(s.cfg.PluginsCDNURLTemplate) + if err != nil { + return "", fmt.Errorf("url parse: %w", err) + } + return u.Scheme + "://" + u.Host, nil +} + +// SystemJSAssetPath returns a system-js path for the specified asset on the plugins CDN. +// It replaces the base path of the CDN with systemJSCDNKeyword. +// If assetPath is an empty string, the base path for the plugin is returned. +func (s *Service) SystemJSAssetPath(pluginID, pluginVersion, assetPath string) (string, error) { + u, err := s.NewCDNURLConstructor(pluginID, pluginVersion).Path(assetPath) + if err != nil { + return "", err + } + return path.Join(systemJSCDNKeyword, u.Path), nil +} + +// AssetURL returns the URL of a CDN asset for a CDN plugin. If the specified plugin is not a CDN plugin, +// it returns ErrPluginNotCDN. +func (s *Service) AssetURL(pluginID, pluginVersion, assetPath string) (string, error) { + if !s.PluginSupported(pluginID) { + return "", ErrPluginNotCDN + } + return s.NewCDNURLConstructor(pluginID, pluginVersion).StringPath(assetPath) +} diff --git a/pkg/plugins/pluginscdn/pluginscdn_test.go b/pkg/plugins/pluginscdn/pluginscdn_test.go new file mode 100644 index 00000000000..b473aeb4b54 --- /dev/null +++ b/pkg/plugins/pluginscdn/pluginscdn_test.go @@ -0,0 +1,49 @@ +package pluginscdn + +import ( + "testing" + + "github.com/grafana/grafana/pkg/plugins/config" + "github.com/stretchr/testify/require" +) + +func TestService(t *testing.T) { + svc := ProvideService(&config.Cfg{ + PluginsCDNURLTemplate: "https://cdn.example.com/{id}/{version}/public/plugins/{id}/{assetPath}", + PluginSettings: map[string]map[string]string{ + "one": {"cdn": "true"}, + "two": {}, + }, + }) + + t.Run("IsCDNPlugin", func(t *testing.T) { + require.True(t, svc.PluginSupported("one")) + require.False(t, svc.PluginSupported("two")) + require.False(t, svc.PluginSupported("unknown")) + }) + + t.Run("CDNBaseURL", func(t *testing.T) { + for _, c := range []struct { + name string + cfgURL string + expBaseURL string + }{ + { + name: "valid", + cfgURL: "https://grafana-assets.grafana.net/plugin-cdn-test/plugin-cdn/{id}/{version}/public/plugins/{id}/{assetPath}", + expBaseURL: "https://grafana-assets.grafana.net", + }, + { + name: "empty", + cfgURL: "", + expBaseURL: "", + }, + } { + t.Run(c.name, func(t *testing.T) { + u, err := ProvideService(&config.Cfg{PluginsCDNURLTemplate: c.cfgURL}).BaseURL() + require.NoError(t, err) + require.Equal(t, c.expBaseURL, u) + }) + } + }) +} diff --git a/pkg/plugins/pluginscdn/url_constructor.go b/pkg/plugins/pluginscdn/url_constructor.go new file mode 100644 index 00000000000..c4ab35ca668 --- /dev/null +++ b/pkg/plugins/pluginscdn/url_constructor.go @@ -0,0 +1,61 @@ +package pluginscdn + +import ( + "fmt" + "net/url" + "path" + "strings" +) + +// URLConstructor is a struct that can build CDN URLs for plugins on a remote CDN. +type URLConstructor struct { + // cdnURLTemplate is absolute base url of the CDN. This string will be formatted + // according to the rules specified in the Path method. + cdnURLTemplate string + + // pluginID is the ID of the plugin. + pluginID string + + // pluginVersion is the version of the plugin. + pluginVersion string +} + +// Path returns a new *url.URL that points to an asset file for the CDN, plugin and plugin version +// specified by the current URLConstructor. +// +// c.cdnURLTemplate is used to build the string, the following substitutions are performed in it: +// +// - {id} -> plugin id +// +// - {version} -> plugin version +// +// - {assetPath} -> assetPath +// +// The asset Path is sanitized via path.Clean (double slashes are removed, "../" is resolved, etc). +// +// The returned URL will be for a file, so it won't have a trailing slash. +func (c URLConstructor) Path(assetPath string) (*url.URL, error) { + u, err := url.Parse( + strings.TrimRight( + strings.NewReplacer( + "{id}", c.pluginID, + "{version}", c.pluginVersion, + "{assetPath}", strings.Trim(path.Clean("/"+assetPath+"/"), "/"), + ).Replace(c.cdnURLTemplate), + "/", + ), + ) + if err != nil { + return nil, fmt.Errorf("url parse: %w", err) + } + return u, nil +} + +// StringPath is like Path, but it returns the absolute URL as a string rather than *url.URL. +func (c URLConstructor) StringPath(assetPath string) (string, error) { + u, err := c.Path(assetPath) + if err != nil { + return "", err + } + return u.String(), nil +} diff --git a/pkg/plugins/pluginscdn/url_constructor_test.go b/pkg/plugins/pluginscdn/url_constructor_test.go new file mode 100644 index 00000000000..aae290c432d --- /dev/null +++ b/pkg/plugins/pluginscdn/url_constructor_test.go @@ -0,0 +1,34 @@ +package pluginscdn + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestURLConstructor_StringURLFor(t *testing.T) { + uc := URLConstructor{ + cdnURLTemplate: "https://the.cdn/{id}/{version}/{assetPath}", + pluginID: "the-plugin", + pluginVersion: "0.1", + } + type tc struct { + name string + path string + exp string + } + for _, c := range []tc{ + {"simple", "file.txt", "https://the.cdn/the-plugin/0.1/file.txt"}, + {"multiple", "some/path/to/file.txt", "https://the.cdn/the-plugin/0.1/some/path/to/file.txt"}, + {"path traversal", "some/../to/file.txt", "https://the.cdn/the-plugin/0.1/to/file.txt"}, + {"above root", "../../../../../file.txt", "https://the.cdn/the-plugin/0.1/file.txt"}, + {"multiple slashes", "some/////file.txt", "https://the.cdn/the-plugin/0.1/some/file.txt"}, + {"dots", "some/././././file.txt", "https://the.cdn/the-plugin/0.1/some/file.txt"}, + } { + t.Run(c.name, func(t *testing.T) { + u, err := uc.StringPath(c.path) + require.NoError(t, err) + require.Equal(t, c.exp, u) + }) + } +} diff --git a/pkg/registry/corekind/base_gen.go b/pkg/registry/corekind/base_gen.go index 8a180db1ca0..99d00e9c0d0 100644 --- a/pkg/registry/corekind/base_gen.go +++ b/pkg/registry/corekind/base_gen.go @@ -13,6 +13,7 @@ import ( "fmt" "github.com/grafana/grafana/pkg/kinds/dashboard" + "github.com/grafana/grafana/pkg/kinds/librarypanel" "github.com/grafana/grafana/pkg/kinds/playlist" "github.com/grafana/grafana/pkg/kinds/preferences" "github.com/grafana/grafana/pkg/kinds/publicdashboard" @@ -35,6 +36,7 @@ import ( type Base struct { all []kindsys.Core dashboard *dashboard.Kind + librarypanel *librarypanel.Kind playlist *playlist.Kind preferences *preferences.Kind publicdashboard *publicdashboard.Kind @@ -45,6 +47,7 @@ type Base struct { // type guards var ( _ kindsys.Core = &dashboard.Kind{} + _ kindsys.Core = &librarypanel.Kind{} _ kindsys.Core = &playlist.Kind{} _ kindsys.Core = &preferences.Kind{} _ kindsys.Core = &publicdashboard.Kind{} @@ -57,6 +60,11 @@ func (b *Base) Dashboard() *dashboard.Kind { return b.dashboard } +// LibraryPanel returns the [kindsys.Interface] implementation for the librarypanel kind. +func (b *Base) LibraryPanel() *librarypanel.Kind { + return b.librarypanel +} + // Playlist returns the [kindsys.Interface] implementation for the playlist kind. func (b *Base) Playlist() *playlist.Kind { return b.playlist @@ -92,6 +100,12 @@ func doNewBase(rt *thema.Runtime) *Base { } reg.all = append(reg.all, reg.dashboard) + reg.librarypanel, err = librarypanel.NewKind(rt) + if err != nil { + panic(fmt.Sprintf("error while initializing the librarypanel Kind: %s", err)) + } + reg.all = append(reg.all, reg.librarypanel) + reg.playlist, err = playlist.NewKind(rt) if err != nil { panic(fmt.Sprintf("error while initializing the playlist Kind: %s", err)) diff --git a/pkg/server/server.go b/pkg/server/server.go index c59a37e1c6f..0c06701694c 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -11,18 +11,17 @@ import ( "strconv" "sync" - "github.com/grafana/grafana/pkg/infra/usagestats/statscollector" - "github.com/grafana/grafana/pkg/services/accesscontrol" + "golang.org/x/sync/errgroup" "github.com/grafana/grafana/pkg/api" _ "github.com/grafana/grafana/pkg/extensions" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/metrics" + "github.com/grafana/grafana/pkg/infra/usagestats/statscollector" "github.com/grafana/grafana/pkg/registry" + "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/provisioning" - "github.com/grafana/grafana/pkg/setting" - "golang.org/x/sync/errgroup" ) // Options contains parameters for the New function. diff --git a/pkg/server/server_test.go b/pkg/server/server_test.go index be4aca1585e..6864cbe589c 100644 --- a/pkg/server/server_test.go +++ b/pkg/server/server_test.go @@ -7,11 +7,12 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/server/backgroundsvcs" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/require" ) type testService struct { diff --git a/pkg/services/accesscontrol/accesscontrol.go b/pkg/services/accesscontrol/accesscontrol.go index ef2ca546a57..16ca6324ee0 100644 --- a/pkg/services/accesscontrol/accesscontrol.go +++ b/pkg/services/accesscontrol/accesscontrol.go @@ -5,8 +5,8 @@ import ( "fmt" "strings" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/registry" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" @@ -100,8 +100,8 @@ type User struct { } // HasGlobalAccess checks user access with globally assigned permissions only -func HasGlobalAccess(ac AccessControl, service Service, c *models.ReqContext) func(fallback func(*models.ReqContext) bool, evaluator Evaluator) bool { - return func(fallback func(*models.ReqContext) bool, evaluator Evaluator) bool { +func HasGlobalAccess(ac AccessControl, service Service, c *contextmodel.ReqContext) func(fallback func(*contextmodel.ReqContext) bool, evaluator Evaluator) bool { + return func(fallback func(*contextmodel.ReqContext) bool, evaluator Evaluator) bool { if ac.IsDisabled() { return fallback(c) } @@ -131,8 +131,8 @@ func HasGlobalAccess(ac AccessControl, service Service, c *models.ReqContext) fu } } -func HasAccess(ac AccessControl, c *models.ReqContext) func(fallback func(*models.ReqContext) bool, evaluator Evaluator) bool { - return func(fallback func(*models.ReqContext) bool, evaluator Evaluator) bool { +func HasAccess(ac AccessControl, c *contextmodel.ReqContext) func(fallback func(*contextmodel.ReqContext) bool, evaluator Evaluator) bool { + return func(fallback func(*contextmodel.ReqContext) bool, evaluator Evaluator) bool { if ac.IsDisabled() { return fallback(c) } @@ -147,31 +147,31 @@ func HasAccess(ac AccessControl, c *models.ReqContext) func(fallback func(*model } } -var ReqSignedIn = func(c *models.ReqContext) bool { +var ReqSignedIn = func(c *contextmodel.ReqContext) bool { return c.IsSignedIn } -var ReqGrafanaAdmin = func(c *models.ReqContext) bool { +var ReqGrafanaAdmin = func(c *contextmodel.ReqContext) bool { return c.IsGrafanaAdmin } // ReqViewer returns true if the current user has org.RoleViewer. Note: this can be anonymous user as well -var ReqViewer = func(c *models.ReqContext) bool { +var ReqViewer = func(c *contextmodel.ReqContext) bool { return c.OrgRole.Includes(org.RoleViewer) } -var ReqOrgAdmin = func(c *models.ReqContext) bool { +var ReqOrgAdmin = func(c *contextmodel.ReqContext) bool { return c.OrgRole == org.RoleAdmin } -var ReqOrgAdminOrEditor = func(c *models.ReqContext) bool { +var ReqOrgAdminOrEditor = func(c *contextmodel.ReqContext) bool { return c.OrgRole == org.RoleAdmin || c.OrgRole == org.RoleEditor } // ReqHasRole generates a fallback to check whether the user has a role // Note that while ReqOrgAdmin returns false for a Grafana Admin / Viewer, ReqHasRole(org.RoleAdmin) will return true -func ReqHasRole(role org.RoleType) func(c *models.ReqContext) bool { - return func(c *models.ReqContext) bool { return c.HasRole(role) } +func ReqHasRole(role org.RoleType) func(c *contextmodel.ReqContext) bool { + return func(c *contextmodel.ReqContext) bool { return c.HasRole(role) } } func BuildPermissionsMap(permissions []Permission) map[string]bool { diff --git a/pkg/services/accesscontrol/acimpl/accesscontrol_test.go b/pkg/services/accesscontrol/acimpl/accesscontrol_test.go index 4c53ee45aea..e3cc5fbcd92 100644 --- a/pkg/services/accesscontrol/acimpl/accesscontrol_test.go +++ b/pkg/services/accesscontrol/acimpl/accesscontrol_test.go @@ -4,10 +4,11 @@ import ( "context" "testing" + "github.com/stretchr/testify/assert" + "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/assert" ) func TestAccessControl_Evaluate(t *testing.T) { diff --git a/pkg/services/accesscontrol/acimpl/service_bench_test.go b/pkg/services/accesscontrol/acimpl/service_bench_test.go index 3d864a3aa28..81e6d0e858f 100644 --- a/pkg/services/accesscontrol/acimpl/service_bench_test.go +++ b/pkg/services/accesscontrol/acimpl/service_bench_test.go @@ -3,78 +3,21 @@ package acimpl import ( "context" "fmt" - "sync" "testing" "time" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/accesscontrol/actest" "github.com/grafana/grafana/pkg/services/accesscontrol/database" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/require" ) -const concurrency = 10 -const batchSize = 1000 - -type bounds struct { - start, end int -} - -// concurrentBatch spawns the requested amount of workers then ask them to run eachFn on chunks of the requested size -func concurrentBatch(workers, count, size int, eachFn func(start, end int) error) error { - var wg sync.WaitGroup - alldone := make(chan bool) // Indicates that all workers have finished working - chunk := make(chan bounds) // Gives the workers the bounds they should work with - ret := make(chan error) // Allow workers to notify in case of errors - defer close(ret) - - // Launch all workers - for x := 0; x < workers; x++ { - wg.Add(1) - go func() { - defer wg.Done() - for ck := range chunk { - if err := eachFn(ck.start, ck.end); err != nil { - ret <- err - return - } - } - }() - } - - go func() { - // Tell the workers the chunks they have to work on - for i := 0; i < count; { - end := i + size - if end > count { - end = count - } - - chunk <- bounds{start: i, end: end} - - i = end - } - close(chunk) - - // Wait for the workers - wg.Wait() - close(alldone) - }() - - // wait for an error or for all workers to be done - select { - case err := <-ret: - return err - case <-alldone: - break - } - return nil -} - // setupBenchEnv will create userCount users, userCount managed roles with resourceCount managed permission each // Example: setupBenchEnv(b, 2, 3): // - will create 2 users and assign them 2 managed roles @@ -102,7 +45,7 @@ func setupBenchEnv(b *testing.B, usersCount, resourceCount int) (accesscontrol.S require.NoError(b, err) // Populate users, roles and assignments - if errInsert := concurrentBatch(concurrency, usersCount, batchSize, func(start, end int) error { + if errInsert := actest.ConcurrentBatch(actest.Concurrency, usersCount, actest.BatchSize, func(start, end int) error { n := end - start users := make([]user.User, 0, n) orgUsers := make([]org.OrgUser, 0, n) @@ -157,13 +100,13 @@ func setupBenchEnv(b *testing.B, usersCount, resourceCount int) (accesscontrol.S }) return err }); errInsert != nil { - require.NoError(b, err, "could not insert users and roles") + require.NoError(b, errInsert, "could not insert users and roles") return nil, nil } // Populate permissions action2 := "resources:action2" - if errInsert := concurrentBatch(concurrency, resourceCount*usersCount, batchSize, func(start, end int) error { + if errInsert := actest.ConcurrentBatch(actest.Concurrency, resourceCount*usersCount, actest.BatchSize, func(start, end int) error { permissions := make([]accesscontrol.Permission, 0, end-start) for i := start; i < end; i++ { permissions = append(permissions, accesscontrol.Permission{ @@ -180,7 +123,7 @@ func setupBenchEnv(b *testing.B, usersCount, resourceCount int) (accesscontrol.S return err }) }); errInsert != nil { - require.NoError(b, err, "could not insert permissions") + require.NoError(b, errInsert, "could not insert permissions") return nil, nil } diff --git a/pkg/services/accesscontrol/actest/common.go b/pkg/services/accesscontrol/actest/common.go new file mode 100644 index 00000000000..5aa71efe08c --- /dev/null +++ b/pkg/services/accesscontrol/actest/common.go @@ -0,0 +1,61 @@ +package actest + +import "sync" + +const Concurrency = 10 +const BatchSize = 1000 + +type bounds struct { + start, end int +} + +// ConcurrentBatch spawns the requested amount of workers then ask them to run eachFn on chunks of the requested size +func ConcurrentBatch(workers, count, size int, eachFn func(start, end int) error) error { + var wg sync.WaitGroup + alldone := make(chan bool) // Indicates that all workers have finished working + chunk := make(chan bounds) // Gives the workers the bounds they should work with + ret := make(chan error) // Allow workers to notify in case of errors + defer close(ret) + + // Launch all workers + for x := 0; x < workers; x++ { + wg.Add(1) + go func() { + defer wg.Done() + for ck := range chunk { + if err := eachFn(ck.start, ck.end); err != nil { + ret <- err + return + } + } + }() + } + + go func() { + // Tell the workers the chunks they have to work on + for i := 0; i < count; { + end := i + size + if end > count { + end = count + } + + chunk <- bounds{start: i, end: end} + + i = end + } + close(chunk) + + // Wait for the workers + wg.Wait() + close(alldone) + }() + + // wait for an error or for all workers to be done + select { + case err := <-ret: + return err + case <-alldone: + break + } + return nil +} diff --git a/pkg/services/accesscontrol/api/api.go b/pkg/services/accesscontrol/api/api.go index a3d279e103f..b0f620dd31b 100644 --- a/pkg/services/accesscontrol/api/api.go +++ b/pkg/services/accesscontrol/api/api.go @@ -7,8 +7,8 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/models" ac "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/web" ) @@ -47,7 +47,7 @@ func (api *AccessControlAPI) RegisterAPIEndpoints() { } // GET /api/access-control/user/actions -func (api *AccessControlAPI) getUserActions(c *models.ReqContext) response.Response { +func (api *AccessControlAPI) getUserActions(c *contextmodel.ReqContext) response.Response { reloadCache := c.QueryBool("reloadcache") permissions, err := api.Service.GetUserPermissions(c.Req.Context(), c.SignedInUser, ac.Options{ReloadCache: reloadCache}) @@ -59,7 +59,7 @@ func (api *AccessControlAPI) getUserActions(c *models.ReqContext) response.Respo } // GET /api/access-control/user/permissions -func (api *AccessControlAPI) getUserPermissions(c *models.ReqContext) response.Response { +func (api *AccessControlAPI) getUserPermissions(c *contextmodel.ReqContext) response.Response { reloadCache := c.QueryBool("reloadcache") permissions, err := api.Service.GetUserPermissions(c.Req.Context(), c.SignedInUser, ac.Options{ReloadCache: reloadCache}) @@ -71,7 +71,7 @@ func (api *AccessControlAPI) getUserPermissions(c *models.ReqContext) response.R } // GET /api/access-control/users/permissions -func (api *AccessControlAPI) searchUsersPermissions(c *models.ReqContext) response.Response { +func (api *AccessControlAPI) searchUsersPermissions(c *contextmodel.ReqContext) response.Response { searchOptions := ac.SearchOptions{ ActionPrefix: c.Query("actionPrefix"), Action: c.Query("action"), @@ -98,7 +98,7 @@ func (api *AccessControlAPI) searchUsersPermissions(c *models.ReqContext) respon } // GET /api/access-control/user/:userID/permissions/search -func (api *AccessControlAPI) searchUserPermissions(c *models.ReqContext) response.Response { +func (api *AccessControlAPI) searchUserPermissions(c *contextmodel.ReqContext) response.Response { userIDString := web.Params(c.Req)[":userID"] userID, err := strconv.ParseInt(userIDString, 10, 64) if err != nil { diff --git a/pkg/services/accesscontrol/api/api_test.go b/pkg/services/accesscontrol/api/api_test.go index 13a2f6161fc..a9313789810 100644 --- a/pkg/services/accesscontrol/api/api_test.go +++ b/pkg/services/accesscontrol/api/api_test.go @@ -5,6 +5,8 @@ import ( "net/http" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/api/routing" ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" @@ -13,7 +15,6 @@ import ( "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/web/webtest" - "github.com/stretchr/testify/require" ) func TestAPI_getUserActions(t *testing.T) { diff --git a/pkg/services/accesscontrol/checker.go b/pkg/services/accesscontrol/checker.go new file mode 100644 index 00000000000..1b0e7d48e03 --- /dev/null +++ b/pkg/services/accesscontrol/checker.go @@ -0,0 +1,57 @@ +package accesscontrol + +import ( + "github.com/grafana/grafana/pkg/services/user" +) + +func Checker(user *user.SignedInUser, action string) func(scopes ...string) bool { + if user.Permissions == nil || user.Permissions[user.OrgID] == nil { + return func(scopes ...string) bool { return false } + } + + userScopes, ok := user.Permissions[user.OrgID][action] + if !ok { + return func(scopes ...string) bool { return false } + } + + lookup := make(map[string]bool, len(userScopes)) + for i := range userScopes { + lookup[userScopes[i]] = true + } + + var checkedWildcards bool + var hasWildcard bool + + return func(scopes ...string) bool { + if !checkedWildcards { + wildcards := wildcardsFromScopes(scopes...) + for _, w := range wildcards { + if _, ok := lookup[w]; ok { + hasWildcard = true + break + } + } + checkedWildcards = true + } + + if hasWildcard { + return true + } + + for _, s := range scopes { + if lookup[s] { + return true + } + } + return false + } +} + +func wildcardsFromScopes(scopes ...string) Wildcards { + prefixes := make([]string, len(scopes)) + for _, scope := range scopes { + prefixes = append(prefixes, ScopePrefix(scope)) + } + + return WildcardsFromPrefixes(prefixes) +} diff --git a/pkg/services/accesscontrol/checker_test.go b/pkg/services/accesscontrol/checker_test.go new file mode 100644 index 00000000000..ad7051d3721 --- /dev/null +++ b/pkg/services/accesscontrol/checker_test.go @@ -0,0 +1,112 @@ +package accesscontrol + +import ( + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/grafana/grafana/pkg/services/user" +) + +type testData struct { + uid string + folderUid string +} + +func (d testData) Scopes() []string { + return []string{ + "dashboards:uid:" + d.uid, + "folders:uid:" + d.folderUid, + } +} + +func generateTestData() []testData { + var data []testData + for i := 1; i < 100; i++ { + data = append(data, testData{ + uid: strconv.Itoa(i), + folderUid: strconv.Itoa(i + 100), + }) + } + return data +} + +func Test_Checker(t *testing.T) { + data := generateTestData() + type testCase struct { + desc string + user *user.SignedInUser + expectedLen int + } + tests := []testCase{ + { + desc: "should pass for every entity with dashboard wildcard scope", + user: &user.SignedInUser{ + OrgID: 1, + Permissions: map[int64]map[string][]string{1: {"dashboards:read": {"dashboards:*"}}}, + }, + expectedLen: len(data), + }, + { + desc: "should pass for every entity with folder wildcard scope", + user: &user.SignedInUser{ + OrgID: 1, + Permissions: map[int64]map[string][]string{1: {"dashboards:read": {"folders:*"}}}, + }, + expectedLen: len(data), + }, + { + desc: "should only pass for for 3 scopes", + user: &user.SignedInUser{ + OrgID: 1, + Permissions: map[int64]map[string][]string{1: {"dashboards:read": {"dashboards:uid:4", "dashboards:uid:50", "dashboards:uid:99"}}}, + }, + expectedLen: 3, + }, + { + desc: "should only pass 4 with secondary supported scope", + user: &user.SignedInUser{ + OrgID: 1, + Permissions: map[int64]map[string][]string{1: {"dashboards:read": {"folders:uid:104", "folders:uid:150", "folders:uid:154", "folders:uid:199"}}}, + }, + expectedLen: 4, + }, + { + desc: "should only pass 4 with some dashboard and some folder scopes", + user: &user.SignedInUser{ + OrgID: 1, + Permissions: map[int64]map[string][]string{1: {"dashboards:read": {"dashboards:uid:1", "dashboards:uid:2", "folders:uid:154", "folders:uid:199"}}}, + }, + expectedLen: 4, + }, + { + desc: "should only pass 2 with overlapping dashboard and folder scopes", + user: &user.SignedInUser{ + OrgID: 1, + Permissions: map[int64]map[string][]string{1: {"dashboards:read": {"dashboards:uid:101", "dashboards:uid:2", "folders:uid:101", "folders:uid:102"}}}, + }, + expectedLen: 2, + }, + { + desc: "should pass none for missing action", + user: &user.SignedInUser{ + OrgID: 1, + Permissions: map[int64]map[string][]string{1: {}}, + }, + expectedLen: 0, + }, + } + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + check := Checker(tt.user, "dashboards:read") + numPasses := 0 + for _, d := range data { + if ok := check(d.Scopes()...); ok { + numPasses++ + } + } + assert.Equal(t, tt.expectedLen, numPasses) + }) + } +} diff --git a/pkg/services/accesscontrol/middleware.go b/pkg/services/accesscontrol/middleware.go index 888fc2a615d..2c3f1299f59 100644 --- a/pkg/services/accesscontrol/middleware.go +++ b/pkg/services/accesscontrol/middleware.go @@ -14,8 +14,8 @@ import ( "time" "github.com/grafana/grafana/pkg/middleware/cookies" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models/usertoken" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" @@ -29,7 +29,7 @@ func Middleware(ac AccessControl) func(web.Handler, Evaluator) web.Handler { return fallback } - return func(c *models.ReqContext) { + return func(c *contextmodel.ReqContext) { if c.AllowAnonymous { forceLogin, _ := strconv.ParseBool(c.Req.URL.Query().Get("forceLogin")) // ignoring error, assuming false for non-true values is ok. orgID, err := strconv.ParseInt(c.Req.URL.Query().Get("orgId"), 10, 64) @@ -53,7 +53,7 @@ func Middleware(ac AccessControl) func(web.Handler, Evaluator) web.Handler { } } -func authorize(c *models.ReqContext, ac AccessControl, user *user.SignedInUser, evaluator Evaluator) { +func authorize(c *contextmodel.ReqContext, ac AccessControl, user *user.SignedInUser, evaluator Evaluator) { injected, err := evaluator.MutateScopes(c.Req.Context(), scopeInjector(scopeParams{ OrgID: c.OrgID, URLParams: web.Params(c.Req), @@ -70,7 +70,7 @@ func authorize(c *models.ReqContext, ac AccessControl, user *user.SignedInUser, } } -func deny(c *models.ReqContext, evaluator Evaluator, err error) { +func deny(c *contextmodel.ReqContext, evaluator Evaluator, err error) { id := newID() if err != nil { c.Logger.Error("Error from access control system", "error", err, "accessErrorID", id) @@ -106,7 +106,7 @@ func deny(c *models.ReqContext, evaluator Evaluator, err error) { }) } -func unauthorized(c *models.ReqContext, err error) { +func unauthorized(c *contextmodel.ReqContext, err error) { if c.IsApiRequest() { response := map[string]interface{}{ "message": "Unauthorized", @@ -129,7 +129,7 @@ func unauthorized(c *models.ReqContext, err error) { c.Redirect(setting.AppSubUrl + "/login") } -func writeRedirectCookie(c *models.ReqContext) { +func writeRedirectCookie(c *contextmodel.ReqContext) { redirectTo := c.Req.RequestURI if setting.AppSubUrl != "" && !strings.HasPrefix(redirectTo, setting.AppSubUrl) { redirectTo = setting.AppSubUrl + c.Req.RequestURI @@ -159,7 +159,7 @@ func newID() string { return "ACE" + id } -type OrgIDGetter func(c *models.ReqContext) (int64, error) +type OrgIDGetter func(c *contextmodel.ReqContext) (int64, error) type userCache interface { GetSignedInUserWithCacheCtx(ctx context.Context, query *user.GetSignedInUserQuery) (*user.SignedInUser, error) @@ -171,7 +171,7 @@ func AuthorizeInOrgMiddleware(ac AccessControl, service Service, cache userCache return fallback } - return func(c *models.ReqContext) { + return func(c *contextmodel.ReqContext) { // using a copy of the user not to modify the signedInUser, yet perform the permission evaluation in another org userCopy := *(c.SignedInUser) orgID, err := getTargetOrg(c) @@ -211,7 +211,7 @@ func AuthorizeInOrgMiddleware(ac AccessControl, service Service, cache userCache } } -func UseOrgFromContextParams(c *models.ReqContext) (int64, error) { +func UseOrgFromContextParams(c *contextmodel.ReqContext) (int64, error) { orgID, err := strconv.ParseInt(web.Params(c.Req)[":orgId"], 10, 64) // Special case of macaron handling invalid params @@ -222,12 +222,12 @@ func UseOrgFromContextParams(c *models.ReqContext) (int64, error) { return orgID, nil } -func UseGlobalOrg(c *models.ReqContext) (int64, error) { +func UseGlobalOrg(c *contextmodel.ReqContext) (int64, error) { return GlobalOrgID, nil } func LoadPermissionsMiddleware(service Service) web.Handler { - return func(c *models.ReqContext) { + return func(c *contextmodel.ReqContext) { if service.IsDisabled() { return } diff --git a/pkg/services/accesscontrol/middleware_test.go b/pkg/services/accesscontrol/middleware_test.go index 7ba3e180126..c9f3626058c 100644 --- a/pkg/services/accesscontrol/middleware_test.go +++ b/pkg/services/accesscontrol/middleware_test.go @@ -8,10 +8,10 @@ import ( "github.com/stretchr/testify/assert" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/web" ) @@ -55,7 +55,7 @@ func TestMiddleware(t *testing.T) { for _, test := range tests { t.Run(test.desc, func(t *testing.T) { fallbackCalled := false - fallback := func(c *models.ReqContext) { + fallback := func(c *contextmodel.ReqContext) { fallbackCalled = true } @@ -66,7 +66,7 @@ func TestMiddleware(t *testing.T) { server.Use(accesscontrol.Middleware(test.ac)(fallback, test.evaluator)) endpointCalled := false - server.Get("/", func(c *models.ReqContext) { + server.Get("/", func(c *contextmodel.ReqContext) { endpointCalled = true c.Resp.WriteHeader(http.StatusOK) }) @@ -99,13 +99,13 @@ func TestMiddleware_forceLogin(t *testing.T) { server := web.New() server.UseMiddleware(web.Renderer("../../public/views", "[[", "]]")) - server.Get("/endpoint", func(c *models.ReqContext) { + server.Get("/endpoint", func(c *contextmodel.ReqContext) { endpointCalled = true c.Resp.WriteHeader(http.StatusOK) }) ac := mock.New().WithPermissions([]accesscontrol.Permission{{Action: "endpoint:read", Scope: "endpoint:1"}}) - server.Use(contextProvider(func(c *models.ReqContext) { + server.Use(contextProvider(func(c *contextmodel.ReqContext) { c.AllowAnonymous = true c.SignedInUser.IsAnonymous = true c.IsSignedIn = false @@ -129,9 +129,9 @@ func TestMiddleware_forceLogin(t *testing.T) { } } -func contextProvider(modifiers ...func(c *models.ReqContext)) web.Handler { +func contextProvider(modifiers ...func(c *contextmodel.ReqContext)) web.Handler { return func(c *web.Context) { - reqCtx := &models.ReqContext{ + reqCtx := &contextmodel.ReqContext{ Context: c, Logger: log.New(""), SignedInUser: &user.SignedInUser{}, diff --git a/pkg/services/accesscontrol/models.go b/pkg/services/accesscontrol/models.go index aed3173e305..b156bfb654d 100644 --- a/pkg/services/accesscontrol/models.go +++ b/pkg/services/accesscontrol/models.go @@ -8,8 +8,11 @@ import ( "github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/util/errutil" ) +var ErrInternal = errutil.NewBase(errutil.StatusInternal, "accesscontrol.internal") + // RoleRegistration stores a role and its assignments to built-in roles // (Viewer, Editor, Admin, Grafana Admin) type RoleRegistration struct { diff --git a/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go b/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go index 81ed7e0ca01..9aa79fe51fb 100644 --- a/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go +++ b/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/licensing" "github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/grafana/grafana/pkg/services/serviceaccounts/retriever" @@ -114,7 +115,7 @@ var DashboardAdminActions = append(DashboardEditActions, []string{dashboards.Act func ProvideDashboardPermissions( cfg *setting.Cfg, router routing.RouteRegister, sql db.DB, ac accesscontrol.AccessControl, - license licensing.Licensing, dashboardStore dashboards.Store, service accesscontrol.Service, + license licensing.Licensing, dashboardStore dashboards.Store, folderService folder.Service, service accesscontrol.Service, teamService team.Service, userService user.Service, ) (*DashboardPermissionsService, error) { getDashboard := func(ctx context.Context, orgID int64, resourceID string) (*dashboards.Dashboard, error) { @@ -152,7 +153,13 @@ func ProvideDashboardPermissions( if err != nil { return nil, err } - return []string{dashboards.ScopeFoldersProvider.GetResourceScopeUID(queryResult.UID)}, nil + parentScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(queryResult.UID) + + nestedScopes, err := dashboards.GetInheritedScopes(ctx, orgID, queryResult.UID, folderService) + if err != nil { + return nil, err + } + return append([]string{parentScope}, nestedScopes...), nil } return []string{}, nil }, @@ -195,7 +202,7 @@ var FolderAdminActions = append(FolderEditActions, []string{dashboards.ActionFol func ProvideFolderPermissions( cfg *setting.Cfg, router routing.RouteRegister, sql db.DB, accesscontrol accesscontrol.AccessControl, - license licensing.Licensing, dashboardStore dashboards.Store, service accesscontrol.Service, + license licensing.Licensing, dashboardStore dashboards.Store, folderService folder.Service, service accesscontrol.Service, teamService team.Service, userService user.Service, ) (*FolderPermissionsService, error) { options := resourcepermissions.Options{ @@ -214,6 +221,9 @@ func ProvideFolderPermissions( return nil }, + InheritedScopesSolver: func(ctx context.Context, orgID int64, resourceID string) ([]string, error) { + return dashboards.GetInheritedScopes(ctx, orgID, resourceID, folderService) + }, Assignments: resourcepermissions.Assignments{ Users: true, Teams: true, diff --git a/pkg/services/accesscontrol/pluginutils/utils_test.go b/pkg/services/accesscontrol/pluginutils/utils_test.go index 0284ebbb49e..c6432a355ab 100644 --- a/pkg/services/accesscontrol/pluginutils/utils_test.go +++ b/pkg/services/accesscontrol/pluginutils/utils_test.go @@ -3,9 +3,10 @@ package pluginutils import ( "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/plugins" ac "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/stretchr/testify/require" ) func TestToRegistrations(t *testing.T) { diff --git a/pkg/services/accesscontrol/resolvers_test.go b/pkg/services/accesscontrol/resolvers_test.go index 0b0b782cd90..310afce1748 100644 --- a/pkg/services/accesscontrol/resolvers_test.go +++ b/pkg/services/accesscontrol/resolvers_test.go @@ -4,10 +4,11 @@ import ( "context" "testing" + "github.com/stretchr/testify/assert" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/datasources" - "github.com/stretchr/testify/assert" ) func TestResolvers_AttributeScope(t *testing.T) { diff --git a/pkg/services/accesscontrol/resourcepermissions/api.go b/pkg/services/accesscontrol/resourcepermissions/api.go index 094221df7e6..71a0a136aa8 100644 --- a/pkg/services/accesscontrol/resourcepermissions/api.go +++ b/pkg/services/accesscontrol/resourcepermissions/api.go @@ -8,8 +8,8 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/web" ) @@ -68,7 +68,7 @@ type Description struct { Permissions []string `json:"permissions"` } -func (a *api) getDescription(c *models.ReqContext) response.Response { +func (a *api) getDescription(c *contextmodel.ReqContext) response.Response { return response.JSON(http.StatusOK, &Description{ Permissions: a.permissions, Assignments: a.service.options.Assignments, @@ -91,7 +91,7 @@ type resourcePermissionDTO struct { Permission string `json:"permission"` } -func (a *api) getPermissions(c *models.ReqContext) response.Response { +func (a *api) getPermissions(c *contextmodel.ReqContext) response.Response { resourceID := web.Params(c.Req)[":resourceID"] permissions, err := a.service.GetPermissions(c.Req.Context(), c.SignedInUser, resourceID) @@ -144,7 +144,7 @@ type setPermissionsCommand struct { Permissions []accesscontrol.SetResourcePermissionCommand `json:"permissions"` } -func (a *api) setUserPermission(c *models.ReqContext) response.Response { +func (a *api) setUserPermission(c *contextmodel.ReqContext) response.Response { userID, err := strconv.ParseInt(web.Params(c.Req)[":userID"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "userID is invalid", err) @@ -164,7 +164,7 @@ func (a *api) setUserPermission(c *models.ReqContext) response.Response { return permissionSetResponse(cmd) } -func (a *api) setTeamPermission(c *models.ReqContext) response.Response { +func (a *api) setTeamPermission(c *contextmodel.ReqContext) response.Response { teamID, err := strconv.ParseInt(web.Params(c.Req)[":teamID"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "teamID is invalid", err) @@ -184,7 +184,7 @@ func (a *api) setTeamPermission(c *models.ReqContext) response.Response { return permissionSetResponse(cmd) } -func (a *api) setBuiltinRolePermission(c *models.ReqContext) response.Response { +func (a *api) setBuiltinRolePermission(c *contextmodel.ReqContext) response.Response { builtInRole := web.Params(c.Req)[":builtInRole"] resourceID := web.Params(c.Req)[":resourceID"] @@ -201,7 +201,7 @@ func (a *api) setBuiltinRolePermission(c *models.ReqContext) response.Response { return permissionSetResponse(cmd) } -func (a *api) setPermissions(c *models.ReqContext) response.Response { +func (a *api) setPermissions(c *contextmodel.ReqContext) response.Response { resourceID := web.Params(c.Req)[":resourceID"] cmd := setPermissionsCommand{} diff --git a/pkg/services/accesscontrol/resourcepermissions/api_test.go b/pkg/services/accesscontrol/resourcepermissions/api_test.go index 955f30a1189..06a918738af 100644 --- a/pkg/services/accesscontrol/resourcepermissions/api_test.go +++ b/pkg/services/accesscontrol/resourcepermissions/api_test.go @@ -15,9 +15,9 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/org/orgimpl" "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/sqlstore" @@ -444,7 +444,7 @@ type testContext struct { func contextProvider(tc *testContext) web.Handler { return func(c *web.Context) { signedIn := tc.user != nil - reqCtx := &models.ReqContext{ + reqCtx := &contextmodel.ReqContext{ Context: c, SignedInUser: tc.user, IsSignedIn: signedIn, diff --git a/pkg/services/accesscontrol/resourcepermissions/middleware.go b/pkg/services/accesscontrol/resourcepermissions/middleware.go index fead18fb9b8..7acb80d3392 100644 --- a/pkg/services/accesscontrol/resourcepermissions/middleware.go +++ b/pkg/services/accesscontrol/resourcepermissions/middleware.go @@ -3,12 +3,12 @@ package resourcepermissions import ( "net/http" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/web" ) func disableMiddleware(shouldDisable bool) web.Handler { - return func(c *models.ReqContext) { + return func(c *contextmodel.ReqContext) { if shouldDisable { c.Resp.WriteHeader(http.StatusNotFound) return @@ -16,4 +16,4 @@ func disableMiddleware(shouldDisable bool) web.Handler { } } -func nopMiddleware(c *models.ReqContext) {} +func nopMiddleware(c *contextmodel.ReqContext) {} diff --git a/pkg/services/accesscontrol/scope.go b/pkg/services/accesscontrol/scope.go index 8b58b9bd8d2..4805b25f2a1 100644 --- a/pkg/services/accesscontrol/scope.go +++ b/pkg/services/accesscontrol/scope.go @@ -142,19 +142,26 @@ func (s scopeProviderImpl) GetResourceAllIDScope() string { return GetResourceAllIDScope(s.root) } -// WildcardsFromPrefix generates valid wildcards from prefix -// datasource:uid: => "*", "datasource:*", "datasource:uid:*" func WildcardsFromPrefix(prefix string) Wildcards { + return WildcardsFromPrefixes([]string{prefix}) +} + +// WildcardsFromPrefixes generates valid wildcards from prefixes +// datasource:uid: => "*", "datasource:*", "datasource:uid:*" +func WildcardsFromPrefixes(prefixes []string) Wildcards { var b strings.Builder wildcards := Wildcards{"*"} - parts := strings.Split(prefix, ":") - for _, p := range parts { - if p == "" { - continue + for _, prefix := range prefixes { + parts := strings.Split(prefix, ":") + for _, p := range parts { + if p == "" { + continue + } + b.WriteString(p) + b.WriteRune(':') + wildcards = append(wildcards, b.String()+"*") } - b.WriteString(p) - b.WriteRune(':') - wildcards = append(wildcards, b.String()+"*") + b.Reset() } return wildcards } diff --git a/pkg/services/alerting/conditions/evaluator_test.go b/pkg/services/alerting/conditions/evaluator_test.go index f7188f8a49c..937c0527525 100644 --- a/pkg/services/alerting/conditions/evaluator_test.go +++ b/pkg/services/alerting/conditions/evaluator_test.go @@ -3,9 +3,10 @@ package conditions import ( "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/components/null" "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/stretchr/testify/require" ) func evaluatorScenario(t *testing.T, json string, reducedValue float64, datapoints ...float64) bool { diff --git a/pkg/services/alerting/conditions/query.go b/pkg/services/alerting/conditions/query.go index a51c9d3b6ed..d16a39d5fa1 100644 --- a/pkg/services/alerting/conditions/query.go +++ b/pkg/services/alerting/conditions/query.go @@ -7,10 +7,6 @@ import ( "strings" "time" - "github.com/grafana/grafana/pkg/tsdb/legacydata" - "github.com/grafana/grafana/pkg/tsdb/legacydata/interval" - "github.com/grafana/grafana/pkg/tsdb/prometheus" - "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana/pkg/components/null" @@ -18,6 +14,9 @@ import ( "github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/datasources" ngalertmodels "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/tsdb/legacydata" + "github.com/grafana/grafana/pkg/tsdb/legacydata/interval" + "github.com/grafana/grafana/pkg/tsdb/prometheus" ) func init() { diff --git a/pkg/services/alerting/conditions/query_interval_test.go b/pkg/services/alerting/conditions/query_interval_test.go index 7fe1f7177fa..9a42504f7f2 100644 --- a/pkg/services/alerting/conditions/query_interval_test.go +++ b/pkg/services/alerting/conditions/query_interval_test.go @@ -4,6 +4,8 @@ import ( "context" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db/dbtest" "github.com/grafana/grafana/pkg/infra/localcache" @@ -14,8 +16,6 @@ import ( "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tsdb/intervalv2" "github.com/grafana/grafana/pkg/tsdb/legacydata" - - "github.com/stretchr/testify/require" ) func TestQueryInterval(t *testing.T) { diff --git a/pkg/services/alerting/conditions/query_test.go b/pkg/services/alerting/conditions/query_test.go index 3def8a5042a..43baf96826a 100644 --- a/pkg/services/alerting/conditions/query_test.go +++ b/pkg/services/alerting/conditions/query_test.go @@ -6,24 +6,22 @@ import ( "testing" "time" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/stretchr/testify/require" + "github.com/xorcare/pointer" + + "github.com/grafana/grafana/pkg/components/null" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db/dbtest" "github.com/grafana/grafana/pkg/infra/localcache" + "github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/datasources" fd "github.com/grafana/grafana/pkg/services/datasources/fakes" "github.com/grafana/grafana/pkg/services/validations" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tsdb/legacydata" - - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" - "github.com/grafana/grafana-plugin-sdk-go/data" - - "github.com/grafana/grafana/pkg/components/null" - "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/services/alerting" - - "github.com/stretchr/testify/require" - "github.com/xorcare/pointer" ) func newTimeSeriesPointsFromArgs(values ...float64) legacydata.DataTimeSeriesPoints { diff --git a/pkg/services/alerting/conditions/reducer.go b/pkg/services/alerting/conditions/reducer.go index 28d2795e267..6fb986cb5aa 100644 --- a/pkg/services/alerting/conditions/reducer.go +++ b/pkg/services/alerting/conditions/reducer.go @@ -2,7 +2,6 @@ package conditions import ( "math" - "sort" "github.com/grafana/grafana/pkg/components/null" diff --git a/pkg/services/alerting/conditions/reducer_test.go b/pkg/services/alerting/conditions/reducer_test.go index 9542953306c..c357a2e9c4b 100644 --- a/pkg/services/alerting/conditions/reducer_test.go +++ b/pkg/services/alerting/conditions/reducer_test.go @@ -4,9 +4,10 @@ import ( "math" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/components/null" "github.com/grafana/grafana/pkg/tsdb/legacydata" - "github.com/stretchr/testify/require" ) func TestSimpleReducer(t *testing.T) { diff --git a/pkg/services/alerting/engine_integration_test.go b/pkg/services/alerting/engine_integration_test.go index 6daf0c29b43..519157b195e 100644 --- a/pkg/services/alerting/engine_integration_test.go +++ b/pkg/services/alerting/engine_integration_test.go @@ -9,6 +9,8 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/infra/usagestats" @@ -17,7 +19,6 @@ import ( encryptionprovider "github.com/grafana/grafana/pkg/services/encryption/provider" encryptionservice "github.com/grafana/grafana/pkg/services/encryption/service" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/require" ) func TestIntegrationEngineTimeouts(t *testing.T) { diff --git a/pkg/services/alerting/eval_handler_test.go b/pkg/services/alerting/eval_handler_test.go index 67baf26cab9..df12cd335e2 100644 --- a/pkg/services/alerting/eval_handler_test.go +++ b/pkg/services/alerting/eval_handler_test.go @@ -4,11 +4,11 @@ import ( "context" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/services/annotations/annotationstest" "github.com/grafana/grafana/pkg/services/validations" "github.com/grafana/grafana/pkg/tsdb/legacydata" - - "github.com/stretchr/testify/require" ) type conditionStub struct { diff --git a/pkg/services/alerting/notifiers/sensu_test.go b/pkg/services/alerting/notifiers/sensu_test.go index 78073b54988..5b6503f798f 100644 --- a/pkg/services/alerting/notifiers/sensu_test.go +++ b/pkg/services/alerting/notifiers/sensu_test.go @@ -3,11 +3,11 @@ package notifiers import ( "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/services/alerting/models" encryptionservice "github.com/grafana/grafana/pkg/services/encryption/service" - - "github.com/stretchr/testify/require" ) func TestSensuNotifier(t *testing.T) { diff --git a/pkg/services/annotations/annotationsimpl/xorm_store_test.go b/pkg/services/annotations/annotationsimpl/xorm_store_test.go index cb752e988ce..2ae6f10ba82 100644 --- a/pkg/services/annotations/annotationsimpl/xorm_store_test.go +++ b/pkg/services/annotations/annotationsimpl/xorm_store_test.go @@ -7,7 +7,6 @@ import ( "testing" "time" - "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -20,6 +19,7 @@ import ( dashboardstore "github.com/grafana/grafana/pkg/services/dashboards/database" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/quota/quotatest" + "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/tag/tagimpl" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" diff --git a/pkg/services/auth/authimpl/auth_token_test.go b/pkg/services/auth/authimpl/auth_token_test.go index 25358aed7dd..4220a30d9cf 100644 --- a/pkg/services/auth/authimpl/auth_token_test.go +++ b/pkg/services/auth/authimpl/auth_token_test.go @@ -8,12 +8,12 @@ import ( "testing" "time" - "github.com/grafana/grafana/pkg/services/auth" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" diff --git a/pkg/services/auth/authtest/testing.go b/pkg/services/auth/authtest/testing.go index d2bbd09b46b..6a924c5ebf4 100644 --- a/pkg/services/auth/authtest/testing.go +++ b/pkg/services/auth/authtest/testing.go @@ -5,11 +5,12 @@ import ( "net" "time" - "github.com/grafana/grafana/pkg/models" + "golang.org/x/oauth2" + "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/datasources" + "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/user" - "golang.org/x/oauth2" ) type FakeUserAuthTokenService struct { @@ -112,7 +113,7 @@ func (s *FakeUserAuthTokenService) BatchRevokeAllUserTokens(ctx context.Context, type FakeOAuthTokenService struct { passThruEnabled bool - ExpectedAuthUser *models.UserAuth + ExpectedAuthUser *login.UserAuth ExpectedErrors map[string]error } @@ -129,7 +130,7 @@ func (ts *FakeOAuthTokenService) IsOAuthPassThruEnabled(*datasources.DataSource) return ts.passThruEnabled } -func (ts *FakeOAuthTokenService) HasOAuthEntry(context.Context, *user.SignedInUser) (*models.UserAuth, bool, error) { +func (ts *FakeOAuthTokenService) HasOAuthEntry(context.Context, *user.SignedInUser) (*login.UserAuth, bool, error) { if ts.ExpectedAuthUser != nil { return ts.ExpectedAuthUser, true, nil } @@ -139,14 +140,14 @@ func (ts *FakeOAuthTokenService) HasOAuthEntry(context.Context, *user.SignedInUs return nil, false, nil } -func (ts *FakeOAuthTokenService) InvalidateOAuthTokens(ctx context.Context, usr *models.UserAuth) error { +func (ts *FakeOAuthTokenService) InvalidateOAuthTokens(ctx context.Context, usr *login.UserAuth) error { ts.ExpectedAuthUser.OAuthAccessToken = "" ts.ExpectedAuthUser.OAuthRefreshToken = "" ts.ExpectedAuthUser.OAuthExpiry = time.Time{} return nil } -func (ts *FakeOAuthTokenService) TryTokenRefresh(ctx context.Context, usr *models.UserAuth) error { +func (ts *FakeOAuthTokenService) TryTokenRefresh(ctx context.Context, usr *login.UserAuth) error { if err, ok := ts.ExpectedErrors["TryTokenRefresh"]; ok { return err } diff --git a/pkg/services/auth/jwt/key_sets.go b/pkg/services/auth/jwt/key_sets.go index 63d4c940e73..175b60c14d2 100644 --- a/pkg/services/auth/jwt/key_sets.go +++ b/pkg/services/auth/jwt/key_sets.go @@ -14,9 +14,10 @@ import ( "os" "time" + jose "gopkg.in/square/go-jose.v2" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/remotecache" - jose "gopkg.in/square/go-jose.v2" ) var ErrFailedToParsePemFile = errors.New("failed to parse pem-encoded file") diff --git a/pkg/services/authn/authn.go b/pkg/services/authn/authn.go index 6767542e2a0..0a762fd9c53 100644 --- a/pkg/services/authn/authn.go +++ b/pkg/services/authn/authn.go @@ -10,8 +10,8 @@ import ( "golang.org/x/oauth2" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/auth" + "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/web" @@ -33,7 +33,7 @@ const ( MetaKeyAuthModule = "authModule" ) -// ClientParams are hints to the auth serviAuthN: Post login hooksce about how to handle the identity management +// ClientParams are hints to the auth service about how to handle the identity management // from the authenticating client. type ClientParams struct { // Update the internal representation of the entity from the identity provided @@ -42,10 +42,10 @@ type ClientParams struct { SyncTeamMembers bool // Create entity in the DB if it doesn't exist AllowSignUp bool - // EnableDisabledUsers is a hint to the auth service that it should reenable disabled users + // EnableDisabledUsers is a hint to the auth service that it should re-enable disabled users EnableDisabledUsers bool // LookUpParams are the arguments used to look up the entity in the DB. - LookUpParams models.UserLookupParams + LookUpParams login.UserLookupParams } type PostAuthHookFn func(ctx context.Context, identity *Identity, r *Request) error @@ -54,14 +54,16 @@ type PostLoginHookFn func(ctx context.Context, identity *Identity, r *Request, e type Service interface { // Authenticate authenticates a request Authenticate(ctx context.Context, r *Request) (*Identity, error) - // RegisterPostAuthHook registers a hook that is called after a successful authentication. - RegisterPostAuthHook(hook PostAuthHookFn) + // RegisterPostAuthHook registers a hook with a priority that is called after a successful authentication. + // A lower number means higher priority. + RegisterPostAuthHook(hook PostAuthHookFn, priority uint) // Login authenticates a request and creates a session on successful authentication. Login(ctx context.Context, client string, r *Request) (*Identity, error) // RegisterPostLoginHook registers a hook that that is called after a login request. - RegisterPostLoginHook(hook PostLoginHookFn) + // A lower number means higher priority. + RegisterPostLoginHook(hook PostLoginHookFn, priority uint) // RedirectURL will generate url that we can use to initiate auth flow for supported clients. - RedirectURL(ctx context.Context, client string, r *Request) (string, error) + RedirectURL(ctx context.Context, client string, r *Request) (*Redirect, error) } type Client interface { @@ -81,7 +83,7 @@ type ContextAwareClient interface { type RedirectClient interface { Client - RedirectURL(ctx context.Context, r *Request) (string, error) + RedirectURL(ctx context.Context, r *Request) (*Redirect, error) } type PasswordClient interface { @@ -120,6 +122,18 @@ func (r *Request) GetMeta(k string) string { return r.metadata[k] } +const ( + KeyOAuthPKCE = "pkce" + KeyOAuthState = "state" +) + +type Redirect struct { + // Url used for redirect + URL string + // Extra contains data used for redirect, e.g. for oauth this would be state and pkce + Extra map[string]string +} + const ( NamespaceUser = "user" NamespaceAPIKey = "api-key" @@ -142,7 +156,7 @@ type Identity struct { ID string // IsAnonymous IsAnonymous bool - // Login is the short hand identifier of the entity. Should be unique. + // Login is the shorthand identifier of the entity. Should be unique. Login string // Name is the display name of the entity. It is not guaranteed to be unique. Name string @@ -245,9 +259,9 @@ func (i *Identity) SignedInUser() *user.SignedInUser { return u } -func (i *Identity) ExternalUserInfo() models.ExternalUserInfo { +func (i *Identity) ExternalUserInfo() login.ExternalUserInfo { _, id := i.NamespacedID() - return models.ExternalUserInfo{ + return login.ExternalUserInfo{ OAuthToken: i.OAuthToken, AuthModule: i.AuthModule, AuthId: i.AuthID, @@ -281,3 +295,8 @@ func IdentityFromSignedInUser(id string, usr *user.SignedInUser, params ClientPa ClientParams: params, } } + +// ClientWithPrefix returns a client name prefixed with "auth.client." +func ClientWithPrefix(name string) string { + return fmt.Sprintf("auth.client.%s", name) +} diff --git a/pkg/services/authn/authnimpl/priority_queue_test.go b/pkg/services/authn/authnimpl/priority_queue_test.go index 119e3a9e5b8..003b858245e 100644 --- a/pkg/services/authn/authnimpl/priority_queue_test.go +++ b/pkg/services/authn/authnimpl/priority_queue_test.go @@ -3,11 +3,11 @@ package authnimpl import ( "testing" - "github.com/grafana/grafana/pkg/services/authn/authntest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/services/authn/authntest" ) func TestQueue(t *testing.T) { diff --git a/pkg/services/authn/authnimpl/service.go b/pkg/services/authn/authnimpl/service.go index 3bddf2f45bb..52ddf7e43a1 100644 --- a/pkg/services/authn/authnimpl/service.go +++ b/pkg/services/authn/authnimpl/service.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/network" "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/login/social" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/apikey" "github.com/grafana/grafana/pkg/services/auth" @@ -52,6 +53,7 @@ func ProvideService( loginAttempts loginattempt.Service, quotaService quota.Service, authInfoService login.AuthInfoService, renderService rendering.Service, features *featuremgmt.FeatureManager, oauthTokenService oauthtoken.OAuthTokenService, + socialService social.Service, ) *Service { s := &Service{ log: log.New("authn.service"), @@ -60,7 +62,8 @@ func ProvideService( clientQueue: newQueue[authn.ContextAwareClient](), tracer: tracer, sessionService: sessionService, - postAuthHooks: []authn.PostAuthHookFn{}, + postAuthHooks: newQueue[authn.PostAuthHookFn](), + postLoginHooks: newQueue[authn.PostLoginHookFn](), } s.RegisterClient(clients.ProvideRender(userService, renderService)) @@ -69,7 +72,7 @@ func ProvideService( if cfg.LoginCookieName != "" { sessionClient := clients.ProvideSession(sessionService, userService, cfg.LoginCookieName, cfg.LoginMaxLifetime) s.RegisterClient(sessionClient) - s.RegisterPostAuthHook(sessionClient.RefreshTokenHook) + s.RegisterPostAuthHook(sessionClient.RefreshTokenHook, 20) } if s.cfg.AnonymousEnabled { @@ -115,16 +118,31 @@ func ProvideService( s.RegisterClient(clients.ProvideJWT(jwtService, cfg)) } + for name := range socialService.GetOAuthProviders() { + oauthCfg := socialService.GetOAuthInfoProvider(name) + if oauthCfg != nil && oauthCfg.Enabled { + clientName := authn.ClientWithPrefix(name) + + connector, errConnector := socialService.GetConnector(name) + httpClient, errHTTPClient := socialService.GetOAuthHttpClient(name) + if errConnector != nil || errHTTPClient != nil { + s.log.Error("failed to configure oauth client", "client", clientName, "err", multierror.Append(errConnector, errHTTPClient)) + } + + s.RegisterClient(clients.ProvideOAuth(clientName, cfg, oauthCfg, connector, httpClient)) + } + } + // FIXME (jguer): move to User package userSyncService := sync.ProvideUserSync(userService, userProtectionService, authInfoService, quotaService) orgUserSyncService := sync.ProvideOrgSync(userService, orgService, accessControlService) - s.RegisterPostAuthHook(userSyncService.SyncUser) - s.RegisterPostAuthHook(orgUserSyncService.SyncOrgUser) - s.RegisterPostAuthHook(sync.ProvideUserLastSeenSync(userService).SyncLastSeen) - s.RegisterPostAuthHook(sync.ProvideAPIKeyLastSeenSync(apikeyService).SyncLastSeen) + s.RegisterPostAuthHook(userSyncService.SyncUser, 10) + s.RegisterPostAuthHook(orgUserSyncService.SyncOrgUser, 30) + s.RegisterPostAuthHook(sync.ProvideUserLastSeenSync(userService).SyncLastSeen, 40) + s.RegisterPostAuthHook(sync.ProvideAPIKeyLastSeenSync(apikeyService).SyncLastSeen, 50) if features.IsEnabled(featuremgmt.FlagAccessTokenExpirationCheck) { - s.RegisterPostAuthHook(sync.ProvideOauthTokenSync(oauthTokenService, sessionService).SyncOauthToken) + s.RegisterPostAuthHook(sync.ProvideOauthTokenSync(oauthTokenService, sessionService).SyncOauthToken, 60) } return s @@ -141,9 +159,9 @@ type Service struct { sessionService auth.UserTokenService // postAuthHooks are called after a successful authentication. They can modify the identity. - postAuthHooks []authn.PostAuthHookFn + postAuthHooks *queue[authn.PostAuthHookFn] // postLoginHooks are called after a login request is performed, both for failing and successful requests. - postLoginHooks []authn.PostLoginHookFn + postLoginHooks *queue[authn.PostLoginHookFn] } func (s *Service) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) { @@ -182,8 +200,8 @@ func (s *Service) authenticate(ctx context.Context, c authn.Client, r *authn.Req return nil, err } - for _, hook := range s.postAuthHooks { - if err := hook(ctx, identity, r); err != nil { + for _, hook := range s.postAuthHooks.items { + if err := hook.v(ctx, identity, r); err != nil { s.log.FromContext(ctx).Warn("post auth hook failed", "error", err, "id", identity) return nil, err } @@ -196,14 +214,14 @@ func (s *Service) authenticate(ctx context.Context, c authn.Client, r *authn.Req return identity, nil } -func (s *Service) RegisterPostAuthHook(hook authn.PostAuthHookFn) { - s.postAuthHooks = append(s.postAuthHooks, hook) +func (s *Service) RegisterPostAuthHook(hook authn.PostAuthHookFn, priority uint) { + s.postAuthHooks.insert(hook, priority) } func (s *Service) Login(ctx context.Context, client string, r *authn.Request) (identity *authn.Identity, err error) { defer func() { - for _, hook := range s.postLoginHooks { - hook(ctx, identity, r, err) + for _, hook := range s.postLoginHooks.items { + hook.v(ctx, identity, r, err) } }() @@ -232,6 +250,7 @@ func (s *Service) Login(ctx context.Context, client string, r *authn.Request) (i sessionToken, err := s.sessionService.CreateToken(ctx, &user.User{ID: id}, ip, r.HTTPRequest.UserAgent()) if err != nil { + s.log.FromContext(ctx).Error("failed to create session", "client", client, "userId", id, "err", err) return nil, err } @@ -239,23 +258,23 @@ func (s *Service) Login(ctx context.Context, client string, r *authn.Request) (i return identity, nil } -func (s *Service) RegisterPostLoginHook(hook authn.PostLoginHookFn) { - s.postLoginHooks = append(s.postLoginHooks, hook) +func (s *Service) RegisterPostLoginHook(hook authn.PostLoginHookFn, priority uint) { + s.postLoginHooks.insert(hook, priority) } -func (s *Service) RedirectURL(ctx context.Context, client string, r *authn.Request) (string, error) { +func (s *Service) RedirectURL(ctx context.Context, client string, r *authn.Request) (*authn.Redirect, error) { ctx, span := s.tracer.Start(ctx, "authn.RedirectURL") defer span.End() span.SetAttributes(attributeKeyClient, client, attribute.Key(attributeKeyClient).String(client)) c, ok := s.clients[client] if !ok { - return "", authn.ErrClientNotConfigured.Errorf("client not configured: %s", client) + return nil, authn.ErrClientNotConfigured.Errorf("client not configured: %s", client) } redirectClient, ok := c.(authn.RedirectClient) if !ok { - return "", authn.ErrUnsupportedClient.Errorf("client does not support generating redirect url: %s", client) + return nil, authn.ErrUnsupportedClient.Errorf("client does not support generating redirect url: %s", client) } return redirectClient.RedirectURL(ctx, r) diff --git a/pkg/services/authn/authnimpl/service_test.go b/pkg/services/authn/authnimpl/service_test.go index 21fb1f50d90..5e6f8631d51 100644 --- a/pkg/services/authn/authnimpl/service_test.go +++ b/pkg/services/authn/authnimpl/service_test.go @@ -243,16 +243,13 @@ func TestService_RedirectURL(t *testing.T) { type testCase struct { desc string client string - expectedURL string expectedErr error } tests := []testCase{ { - desc: "should generate url for valid redirect client", - client: "redirect", - expectedURL: "https://localhost/redirect", - expectedErr: nil, + desc: "should generate url for valid redirect client", + client: "redirect", }, { desc: "should return error on non existing client", @@ -269,13 +266,12 @@ func TestService_RedirectURL(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { service := setupTests(t, func(svc *Service) { - svc.RegisterClient(authntest.FakeRedirectClient{ExpectedName: "redirect", ExpectedURL: tt.expectedURL}) + svc.RegisterClient(authntest.FakeRedirectClient{ExpectedName: "redirect"}) svc.RegisterClient(&authntest.FakeClient{ExpectedName: "non-redirect"}) }) - u, err := service.RedirectURL(context.Background(), tt.client, nil) + _, err := service.RedirectURL(context.Background(), tt.client, nil) assert.ErrorIs(t, err, tt.expectedErr) - assert.Equal(t, tt.expectedURL, u) }) } } @@ -292,11 +288,13 @@ func setupTests(t *testing.T, opts ...func(svc *Service)) *Service { t.Helper() s := &Service{ - log: log.NewNopLogger(), - cfg: setting.NewCfg(), - clientQueue: newQueue[authn.ContextAwareClient](), - clients: map[string]authn.Client{}, - tracer: tracing.InitializeTracerForTest(), + log: log.NewNopLogger(), + cfg: setting.NewCfg(), + clients: map[string]authn.Client{}, + clientQueue: newQueue[authn.ContextAwareClient](), + tracer: tracing.InitializeTracerForTest(), + postAuthHooks: newQueue[authn.PostAuthHookFn](), + postLoginHooks: newQueue[authn.PostLoginHookFn](), } for _, o := range opts { diff --git a/pkg/services/authn/authnimpl/sync/oauth_token_sync_test.go b/pkg/services/authn/authnimpl/sync/oauth_token_sync_test.go index 27a06408674..a180cab31a1 100644 --- a/pkg/services/authn/authnimpl/sync/oauth_token_sync_test.go +++ b/pkg/services/authn/authnimpl/sync/oauth_token_sync_test.go @@ -9,10 +9,10 @@ import ( "github.com/stretchr/testify/assert" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/auth/authtest" "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/oauthtoken/oauthtokentest" "github.com/grafana/grafana/pkg/services/user" ) @@ -22,7 +22,7 @@ func TestOauthTokenSync_SyncOauthToken(t *testing.T) { desc string identity *authn.Identity - expectedHasEntryToken *models.UserAuth + expectedHasEntryToken *login.UserAuth expectHasEntryCalled bool expectedTryRefreshErr error @@ -52,26 +52,26 @@ func TestOauthTokenSync_SyncOauthToken(t *testing.T) { desc: "should skip sync for when access token don't have expire time", identity: &authn.Identity{ID: "user:1", SessionToken: &auth.UserToken{}}, expectHasEntryCalled: true, - expectedHasEntryToken: &models.UserAuth{}, + expectedHasEntryToken: &login.UserAuth{}, }, { desc: "should skip sync when access token has no expired yet", identity: &authn.Identity{ID: "user:1", SessionToken: &auth.UserToken{}}, expectHasEntryCalled: true, - expectedHasEntryToken: &models.UserAuth{OAuthExpiry: time.Now().Add(10 * time.Minute)}, + expectedHasEntryToken: &login.UserAuth{OAuthExpiry: time.Now().Add(10 * time.Minute)}, }, { desc: "should skip sync when access token has no expired yet", identity: &authn.Identity{ID: "user:1", SessionToken: &auth.UserToken{}}, expectHasEntryCalled: true, - expectedHasEntryToken: &models.UserAuth{OAuthExpiry: time.Now().Add(10 * time.Minute)}, + expectedHasEntryToken: &login.UserAuth{OAuthExpiry: time.Now().Add(10 * time.Minute)}, }, { desc: "should refresh access token when is has expired", identity: &authn.Identity{ID: "user:1", SessionToken: &auth.UserToken{}}, expectHasEntryCalled: true, expectTryRefreshTokenCalled: true, - expectedHasEntryToken: &models.UserAuth{OAuthExpiry: time.Now().Add(-10 * time.Minute)}, + expectedHasEntryToken: &login.UserAuth{OAuthExpiry: time.Now().Add(-10 * time.Minute)}, }, { desc: "should invalidate access token and session token if access token can't be refreshed", @@ -81,7 +81,7 @@ func TestOauthTokenSync_SyncOauthToken(t *testing.T) { expectTryRefreshTokenCalled: true, expectInvalidateOauthTokensCalled: true, expectRevokeTokenCalled: true, - expectedHasEntryToken: &models.UserAuth{OAuthExpiry: time.Now().Add(-10 * time.Minute)}, + expectedHasEntryToken: &login.UserAuth{OAuthExpiry: time.Now().Add(-10 * time.Minute)}, expectedErr: errExpiredAccessToken, }, } @@ -96,15 +96,15 @@ func TestOauthTokenSync_SyncOauthToken(t *testing.T) { ) service := &oauthtokentest.MockOauthTokenService{ - HasOAuthEntryFunc: func(ctx context.Context, usr *user.SignedInUser) (*models.UserAuth, bool, error) { + HasOAuthEntryFunc: func(ctx context.Context, usr *user.SignedInUser) (*login.UserAuth, bool, error) { hasEntryCalled = true return tt.expectedHasEntryToken, tt.expectedHasEntryToken != nil, nil }, - InvalidateOAuthTokensFunc: func(ctx context.Context, usr *models.UserAuth) error { + InvalidateOAuthTokensFunc: func(ctx context.Context, usr *login.UserAuth) error { invalidateTokensCalled = true return nil }, - TryTokenRefreshFunc: func(ctx context.Context, usr *models.UserAuth) error { + TryTokenRefreshFunc: func(ctx context.Context, usr *login.UserAuth) error { tryRefreshCalled = true return tt.expectedTryRefreshErr }, diff --git a/pkg/services/authn/authnimpl/sync/org_sync_test.go b/pkg/services/authn/authnimpl/sync/org_sync_test.go index 4fd87ba7d9a..4c4f6d6ea06 100644 --- a/pkg/services/authn/authnimpl/sync/org_sync_test.go +++ b/pkg/services/authn/authnimpl/sync/org_sync_test.go @@ -4,17 +4,18 @@ import ( "context" "testing" + "github.com/stretchr/testify/assert" + "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models/roletype" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/services/login" "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/stretchr/testify/assert" ) func TestOrgSync_SyncOrgUser(t *testing.T) { @@ -79,7 +80,7 @@ func TestOrgSync_SyncOrgUser(t *testing.T) { IsGrafanaAdmin: ptrBool(false), ClientParams: authn.ClientParams{ SyncUser: true, - LookUpParams: models.UserLookupParams{ + LookUpParams: login.UserLookupParams{ UserID: nil, Email: ptrString("test"), Login: nil, @@ -97,7 +98,7 @@ func TestOrgSync_SyncOrgUser(t *testing.T) { IsGrafanaAdmin: ptrBool(false), ClientParams: authn.ClientParams{ SyncUser: true, - LookUpParams: models.UserLookupParams{ + LookUpParams: login.UserLookupParams{ UserID: nil, Email: ptrString("test"), Login: nil, diff --git a/pkg/services/authn/authnimpl/sync/user_sync.go b/pkg/services/authn/authnimpl/sync/user_sync.go index f1740a33b0b..a054a7032ad 100644 --- a/pkg/services/authn/authnimpl/sync/user_sync.go +++ b/pkg/services/authn/authnimpl/sync/user_sync.go @@ -6,7 +6,6 @@ import ( "fmt" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" @@ -140,7 +139,7 @@ func (s *UserSync) updateAuthInfo(ctx context.Context, id *authn.Identity) error return fmt.Errorf("invalid namespace %q for user ID %q", namespace, userID) } - updateCmd := &models.UpdateAuthInfoCommand{ + updateCmd := &login.UpdateAuthInfoCommand{ AuthModule: id.AuthModule, AuthId: id.AuthID, UserId: userID, @@ -222,7 +221,7 @@ func (s *UserSync) createUser(ctx context.Context, id *authn.Identity) (*user.Us } if id.AuthModule != "" && id.AuthID != "" { - if errSetAuth := s.authInfoService.SetAuthInfo(ctx, &models.SetAuthInfoCommand{ + if errSetAuth := s.authInfoService.SetAuthInfo(ctx, &login.SetAuthInfoCommand{ UserId: usr.ID, AuthModule: id.AuthModule, AuthId: id.AuthID, @@ -241,10 +240,10 @@ func (s *UserSync) createUser(ctx context.Context, id *authn.Identity) (*user.Us func (s *UserSync) UserInDB(ctx context.Context, authID *string, authModule *string, - params models.UserLookupParams) (*user.User, error) { + params login.UserLookupParams) (*user.User, error) { // Check authinfo table if authID != nil && authModule != nil { - query := &models.GetAuthInfoQuery{ + query := &login.GetAuthInfoQuery{ AuthModule: *authModule, AuthId: *authID, } @@ -269,7 +268,7 @@ func (s *UserSync) UserInDB(ctx context.Context, return s.LookupByOneOf(ctx, ¶ms) } -func (s *UserSync) LookupByOneOf(ctx context.Context, params *models.UserLookupParams) (*user.User, error) { +func (s *UserSync) LookupByOneOf(ctx context.Context, params *login.UserLookupParams) (*user.User, error) { var usr *user.User var err error diff --git a/pkg/services/authn/authnimpl/sync/user_sync_test.go b/pkg/services/authn/authnimpl/sync/user_sync_test.go index cb92d1112e2..544419c76a8 100644 --- a/pkg/services/authn/authnimpl/sync/user_sync_test.go +++ b/pkg/services/authn/authnimpl/sync/user_sync_test.go @@ -4,7 +4,8 @@ import ( "context" "testing" - "github.com/grafana/grafana/pkg/models" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/login/authinfoservice" @@ -13,7 +14,6 @@ import ( "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/usertest" - "github.com/stretchr/testify/require" ) func ptrString(s string) *string { @@ -34,17 +34,17 @@ func TestUserSync_SyncUser(t *testing.T) { authFakeNil := &logintest.AuthInfoServiceFake{ ExpectedUser: nil, ExpectedError: user.ErrUserNotFound, - SetAuthInfoFn: func(ctx context.Context, cmd *models.SetAuthInfoCommand) error { + SetAuthInfoFn: func(ctx context.Context, cmd *login.SetAuthInfoCommand) error { return nil }, - UpdateAuthInfoFn: func(ctx context.Context, cmd *models.UpdateAuthInfoCommand) error { + UpdateAuthInfoFn: func(ctx context.Context, cmd *login.UpdateAuthInfoCommand) error { return nil }, } authFakeUserID := &logintest.AuthInfoServiceFake{ ExpectedUser: nil, ExpectedError: nil, - ExpectedUserAuth: &models.UserAuth{ + ExpectedUserAuth: &login.UserAuth{ AuthModule: "oauth", AuthId: "2032", UserId: 1, @@ -111,7 +111,7 @@ func TestUserSync_SyncUser(t *testing.T) { Name: "test", Email: "test", ClientParams: authn.ClientParams{ - LookUpParams: models.UserLookupParams{ + LookUpParams: login.UserLookupParams{ UserID: nil, Email: ptrString("test"), Login: nil, @@ -126,7 +126,7 @@ func TestUserSync_SyncUser(t *testing.T) { Name: "test", Email: "test", ClientParams: authn.ClientParams{ - LookUpParams: models.UserLookupParams{ + LookUpParams: login.UserLookupParams{ UserID: nil, Email: ptrString("test"), Login: nil, @@ -150,7 +150,7 @@ func TestUserSync_SyncUser(t *testing.T) { Email: "test", ClientParams: authn.ClientParams{ SyncUser: true, - LookUpParams: models.UserLookupParams{ + LookUpParams: login.UserLookupParams{ UserID: nil, Email: ptrString("test"), Login: nil, @@ -167,7 +167,7 @@ func TestUserSync_SyncUser(t *testing.T) { IsGrafanaAdmin: ptrBool(false), ClientParams: authn.ClientParams{ SyncUser: true, - LookUpParams: models.UserLookupParams{ + LookUpParams: login.UserLookupParams{ UserID: nil, Email: ptrString("test"), Login: nil, @@ -191,7 +191,7 @@ func TestUserSync_SyncUser(t *testing.T) { Email: "test", ClientParams: authn.ClientParams{ SyncUser: true, - LookUpParams: models.UserLookupParams{ + LookUpParams: login.UserLookupParams{ UserID: nil, Email: nil, Login: ptrString("test"), @@ -207,7 +207,7 @@ func TestUserSync_SyncUser(t *testing.T) { Email: "test", IsGrafanaAdmin: ptrBool(false), ClientParams: authn.ClientParams{ - LookUpParams: models.UserLookupParams{ + LookUpParams: login.UserLookupParams{ UserID: nil, Email: nil, Login: ptrString("test"), @@ -232,7 +232,7 @@ func TestUserSync_SyncUser(t *testing.T) { Email: "test", ClientParams: authn.ClientParams{ SyncUser: true, - LookUpParams: models.UserLookupParams{ + LookUpParams: login.UserLookupParams{ UserID: ptrInt64(1), Email: nil, Login: nil, @@ -249,7 +249,7 @@ func TestUserSync_SyncUser(t *testing.T) { IsGrafanaAdmin: ptrBool(false), ClientParams: authn.ClientParams{ SyncUser: true, - LookUpParams: models.UserLookupParams{ + LookUpParams: login.UserLookupParams{ UserID: ptrInt64(1), Email: nil, Login: nil, @@ -274,7 +274,7 @@ func TestUserSync_SyncUser(t *testing.T) { Email: "test", ClientParams: authn.ClientParams{ SyncUser: true, - LookUpParams: models.UserLookupParams{ + LookUpParams: login.UserLookupParams{ UserID: nil, Email: nil, Login: nil, @@ -291,7 +291,7 @@ func TestUserSync_SyncUser(t *testing.T) { IsGrafanaAdmin: ptrBool(false), ClientParams: authn.ClientParams{ SyncUser: true, - LookUpParams: models.UserLookupParams{ + LookUpParams: login.UserLookupParams{ UserID: nil, Email: nil, Login: nil, @@ -317,7 +317,7 @@ func TestUserSync_SyncUser(t *testing.T) { AuthID: "2032", ClientParams: authn.ClientParams{ SyncUser: true, - LookUpParams: models.UserLookupParams{ + LookUpParams: login.UserLookupParams{ UserID: nil, Email: nil, Login: nil, @@ -348,7 +348,7 @@ func TestUserSync_SyncUser(t *testing.T) { SyncUser: true, AllowSignUp: true, EnableDisabledUsers: true, - LookUpParams: models.UserLookupParams{ + LookUpParams: login.UserLookupParams{ UserID: nil, Email: ptrString("test_create"), Login: nil, @@ -369,7 +369,7 @@ func TestUserSync_SyncUser(t *testing.T) { SyncUser: true, AllowSignUp: true, EnableDisabledUsers: true, - LookUpParams: models.UserLookupParams{ + LookUpParams: login.UserLookupParams{ UserID: nil, Email: ptrString("test_create"), Login: nil, @@ -396,7 +396,7 @@ func TestUserSync_SyncUser(t *testing.T) { ClientParams: authn.ClientParams{ SyncUser: true, EnableDisabledUsers: true, - LookUpParams: models.UserLookupParams{ + LookUpParams: login.UserLookupParams{ UserID: ptrInt64(3), Email: nil, Login: nil, @@ -415,7 +415,7 @@ func TestUserSync_SyncUser(t *testing.T) { ClientParams: authn.ClientParams{ SyncUser: true, EnableDisabledUsers: true, - LookUpParams: models.UserLookupParams{ + LookUpParams: login.UserLookupParams{ UserID: ptrInt64(3), Email: nil, Login: nil, diff --git a/pkg/services/authn/authntest/fake.go b/pkg/services/authn/authntest/fake.go index a048bf6a091..a3e119ead9e 100644 --- a/pkg/services/authn/authntest/fake.go +++ b/pkg/services/authn/authntest/fake.go @@ -53,6 +53,8 @@ type FakeRedirectClient struct { ExpectedErr error ExpectedURL string ExpectedName string + ExpectedOK bool + ExpectedRedirect *authn.Redirect ExpectedIdentity *authn.Identity } @@ -64,6 +66,10 @@ func (f FakeRedirectClient) Authenticate(ctx context.Context, r *authn.Request) return f.ExpectedIdentity, f.ExpectedErr } -func (f FakeRedirectClient) RedirectURL(ctx context.Context, r *authn.Request) (string, error) { - return f.ExpectedURL, f.ExpectedErr +func (f FakeRedirectClient) RedirectURL(ctx context.Context, r *authn.Request) (*authn.Redirect, error) { + return f.ExpectedRedirect, f.ExpectedErr +} + +func (f FakeRedirectClient) Test(ctx context.Context, r *authn.Request) bool { + return f.ExpectedOK } diff --git a/pkg/services/authn/clients/anonymous_test.go b/pkg/services/authn/clients/anonymous_test.go index 7ecca2bc583..70867622d9b 100644 --- a/pkg/services/authn/clients/anonymous_test.go +++ b/pkg/services/authn/clients/anonymous_test.go @@ -5,13 +5,14 @@ import ( "fmt" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgtest" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestAnonymous_Authenticate(t *testing.T) { diff --git a/pkg/services/authn/clients/basic_test.go b/pkg/services/authn/clients/basic_test.go index 81b974ee20d..fbf2a96a2d7 100644 --- a/pkg/services/authn/clients/basic_test.go +++ b/pkg/services/authn/clients/basic_test.go @@ -5,9 +5,10 @@ import ( "net/http" "testing" + "github.com/stretchr/testify/assert" + "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/authn/authntest" - "github.com/stretchr/testify/assert" ) func TestBasic_Authenticate(t *testing.T) { diff --git a/pkg/services/authn/clients/grafana_test.go b/pkg/services/authn/clients/grafana_test.go index 918888f5a23..9bf3ce88a13 100644 --- a/pkg/services/authn/clients/grafana_test.go +++ b/pkg/services/authn/clients/grafana_test.go @@ -5,14 +5,15 @@ import ( "net/http" "testing" - "github.com/grafana/grafana/pkg/models" + "github.com/stretchr/testify/assert" + "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/usertest" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" - "github.com/stretchr/testify/assert" ) func TestGrafana_AuthenticateProxy(t *testing.T) { @@ -51,7 +52,7 @@ func TestGrafana_AuthenticateProxy(t *testing.T) { SyncUser: true, SyncTeamMembers: true, AllowSignUp: true, - LookUpParams: models.UserLookupParams{ + LookUpParams: login.UserLookupParams{ Email: strPtr("email@email.com"), Login: strPtr("test"), }, @@ -72,7 +73,7 @@ func TestGrafana_AuthenticateProxy(t *testing.T) { SyncUser: true, SyncTeamMembers: true, AllowSignUp: true, - LookUpParams: models.UserLookupParams{ + LookUpParams: login.UserLookupParams{ Email: strPtr("test@test.com"), Login: strPtr("test@test.com"), }, diff --git a/pkg/services/authn/clients/jwt.go b/pkg/services/authn/clients/jwt.go index 79259943af1..8ad47e3105e 100644 --- a/pkg/services/authn/clients/jwt.go +++ b/pkg/services/authn/clients/jwt.go @@ -69,8 +69,9 @@ func (s *JWT) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identi AuthID: sub, OrgRoles: map[int64]org.RoleType{}, ClientParams: authn.ClientParams{ - SyncUser: true, - SyncTeamMembers: true, + SyncUser: true, + // We do not allow team member sync from JWT Authentication + SyncTeamMembers: false, AllowSignUp: s.cfg.JWTAuthAutoSignUp, EnableDisabledUsers: false, }} diff --git a/pkg/services/authn/clients/jwt_test.go b/pkg/services/authn/clients/jwt_test.go index 80df9289092..29035b19145 100644 --- a/pkg/services/authn/clients/jwt_test.go +++ b/pkg/services/authn/clients/jwt_test.go @@ -10,10 +10,10 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models/roletype" "github.com/grafana/grafana/pkg/services/auth/jwt" "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/setting" ) @@ -49,10 +49,10 @@ func TestAuthenticateJWT(t *testing.T) { IsDisabled: false, HelpFlags1: 0, ClientParams: authn.ClientParams{ + SyncTeamMembers: false, SyncUser: true, AllowSignUp: true, - SyncTeamMembers: true, - LookUpParams: models.UserLookupParams{ + LookUpParams: login.UserLookupParams{ UserID: nil, Email: stringPtr("eai.doe@cor.po"), Login: stringPtr("eai-doe"), diff --git a/pkg/services/authn/clients/ldap.go b/pkg/services/authn/clients/ldap.go index 0e8f14a5be0..943601abf42 100644 --- a/pkg/services/authn/clients/ldap.go +++ b/pkg/services/authn/clients/ldap.go @@ -4,8 +4,8 @@ import ( "context" "errors" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/multildap" "github.com/grafana/grafana/pkg/setting" ) @@ -36,7 +36,7 @@ func (c *LDAP) AuthenticateProxy(ctx context.Context, r *authn.Request, username } func (c *LDAP) AuthenticatePassword(ctx context.Context, r *authn.Request, username, password string) (*authn.Identity, error) { - info, err := c.service.Login(&models.LoginUserQuery{ + info, err := c.service.Login(&login.LoginUserQuery{ Username: username, Password: password, }) @@ -61,8 +61,8 @@ func (c *LDAP) AuthenticatePassword(ctx context.Context, r *authn.Request, usern } type ldapService interface { - Login(query *models.LoginUserQuery) (*models.ExternalUserInfo, error) - User(username string) (*models.ExternalUserInfo, error) + Login(query *login.LoginUserQuery) (*login.ExternalUserInfo, error) + User(username string) (*login.ExternalUserInfo, error) } // FIXME: remove the implementation if we convert ldap to an actual service @@ -70,7 +70,7 @@ type ldapServiceImpl struct { cfg *setting.Cfg } -func (s *ldapServiceImpl) Login(query *models.LoginUserQuery) (*models.ExternalUserInfo, error) { +func (s *ldapServiceImpl) Login(query *login.LoginUserQuery) (*login.ExternalUserInfo, error) { cfg, err := multildap.GetConfig(s.cfg) if err != nil { return nil, err @@ -79,7 +79,7 @@ func (s *ldapServiceImpl) Login(query *models.LoginUserQuery) (*models.ExternalU return multildap.New(cfg.Servers).Login(query) } -func (s *ldapServiceImpl) User(username string) (*models.ExternalUserInfo, error) { +func (s *ldapServiceImpl) User(username string) (*login.ExternalUserInfo, error) { cfg, err := multildap.GetConfig(s.cfg) if err != nil { return nil, err @@ -89,7 +89,7 @@ func (s *ldapServiceImpl) User(username string) (*models.ExternalUserInfo, error return user, err } -func identityFromLDAPInfo(orgID int64, info *models.ExternalUserInfo, allowSignup bool) *authn.Identity { +func identityFromLDAPInfo(orgID int64, info *login.ExternalUserInfo, allowSignup bool) *authn.Identity { return &authn.Identity{ OrgID: orgID, OrgRoles: info.OrgRoles, @@ -105,7 +105,7 @@ func identityFromLDAPInfo(orgID int64, info *models.ExternalUserInfo, allowSignu SyncTeamMembers: true, AllowSignUp: allowSignup, EnableDisabledUsers: true, - LookUpParams: models.UserLookupParams{ + LookUpParams: login.UserLookupParams{ Login: &info.Login, Email: &info.Email, }, diff --git a/pkg/services/authn/clients/ldap_test.go b/pkg/services/authn/clients/ldap_test.go index e59516f2cf1..e5c59207320 100644 --- a/pkg/services/authn/clients/ldap_test.go +++ b/pkg/services/authn/clients/ldap_test.go @@ -4,14 +4,14 @@ import ( "context" "testing" - "github.com/grafana/grafana/pkg/models" + "github.com/stretchr/testify/assert" + "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/ldap" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/multildap" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/assert" ) func TestLDAP_AuthenticateProxy(t *testing.T) { @@ -19,7 +19,7 @@ func TestLDAP_AuthenticateProxy(t *testing.T) { desc string username string expectedLDAPErr error - expectedLDAPInfo *models.ExternalUserInfo + expectedLDAPInfo *login.ExternalUserInfo expectedErr error expectedIdentity *authn.Identity } @@ -28,7 +28,7 @@ func TestLDAP_AuthenticateProxy(t *testing.T) { { desc: "should return valid identity when found by ldap service", username: "test", - expectedLDAPInfo: &models.ExternalUserInfo{ + expectedLDAPInfo: &login.ExternalUserInfo{ AuthModule: login.LDAPAuthModule, AuthId: "123", Email: "test@test.com", @@ -51,7 +51,7 @@ func TestLDAP_AuthenticateProxy(t *testing.T) { SyncTeamMembers: true, AllowSignUp: false, EnableDisabledUsers: true, - LookUpParams: models.UserLookupParams{ + LookUpParams: login.UserLookupParams{ Email: strPtr("test@test.com"), Login: strPtr("test"), }, @@ -83,7 +83,7 @@ func TestLDAP_AuthenticatePassword(t *testing.T) { password string expectedErr error expectedLDAPErr error - expectedLDAPInfo *models.ExternalUserInfo + expectedLDAPInfo *login.ExternalUserInfo expectedIdentity *authn.Identity } @@ -92,7 +92,7 @@ func TestLDAP_AuthenticatePassword(t *testing.T) { desc: "should successfully authenticate with correct username and password", username: "test", password: "test123", - expectedLDAPInfo: &models.ExternalUserInfo{ + expectedLDAPInfo: &login.ExternalUserInfo{ AuthModule: login.LDAPAuthModule, AuthId: "123", Email: "test@test.com", @@ -115,7 +115,7 @@ func TestLDAP_AuthenticatePassword(t *testing.T) { SyncTeamMembers: true, AllowSignUp: false, EnableDisabledUsers: true, - LookUpParams: models.UserLookupParams{ + LookUpParams: login.UserLookupParams{ Email: strPtr("test@test.com"), Login: strPtr("test"), }, @@ -157,13 +157,13 @@ var _ ldapService = new(fakeLDAPService) type fakeLDAPService struct { ExpectedErr error - ExpectedInfo *models.ExternalUserInfo + ExpectedInfo *login.ExternalUserInfo } -func (f fakeLDAPService) Login(query *models.LoginUserQuery) (*models.ExternalUserInfo, error) { +func (f fakeLDAPService) Login(query *login.LoginUserQuery) (*login.ExternalUserInfo, error) { return f.ExpectedInfo, f.ExpectedErr } -func (f fakeLDAPService) User(username string) (*models.ExternalUserInfo, error) { +func (f fakeLDAPService) User(username string) (*login.ExternalUserInfo, error) { return f.ExpectedInfo, f.ExpectedErr } diff --git a/pkg/services/authn/clients/oauth.go b/pkg/services/authn/clients/oauth.go new file mode 100644 index 00000000000..0c99385ee6f --- /dev/null +++ b/pkg/services/authn/clients/oauth.go @@ -0,0 +1,237 @@ +package clients + +import ( + "context" + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "fmt" + "net/http" + "strings" + + "golang.org/x/oauth2" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/login/social" + "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/services/login" + "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util/errutil" +) + +const ( + hostedDomainParamName = "hd" + codeVerifierParamName = "code_verifier" + codeChallengeParamName = "code_challenge" + codeChallengeMethodParamName = "code_challenge_method" + codeChallengeMethod = "S256" + + oauthStateQueryName = "state" + oauthStateCookieName = "oauth_state" + oauthPKCECookieName = "oauth_code_verifier" +) + +var ( + errOAuthGenPKCE = errutil.NewBase(errutil.StatusInternal, "auth.oauth.pkce.internal", errutil.WithPublicMessage("An internal error occurred")) + errOAuthMissingPKCE = errutil.NewBase(errutil.StatusBadRequest, "auth.oauth.pkce.missing", errutil.WithPublicMessage("Missing required pkce cookie")) + + errOAuthGenState = errutil.NewBase(errutil.StatusInternal, "auth.oauth.state.internal", errutil.WithPublicMessage("An internal error occurred")) + errOAuthMissingState = errutil.NewBase(errutil.StatusBadRequest, "auth.oauth.state.missing", errutil.WithPublicMessage("Missing saved oauth state")) + errOAuthInvalidState = errutil.NewBase(errutil.StatusUnauthorized, "auth.oauth.state.invalid", errutil.WithPublicMessage("Provided state does not match stored state")) + + errOAuthTokenExchange = errutil.NewBase(errutil.StatusInternal, "auth.oauth.token.exchange", errutil.WithPublicMessage("Failed to get token from provider")) + + errOAuthMissingRequiredEmail = errutil.NewBase(errutil.StatusUnauthorized, "auth.oauth.email.missing") + errOAuthEmailNotAllowed = errutil.NewBase(errutil.StatusUnauthorized, "auth.oauth.email.not-allowed") +) + +var _ authn.RedirectClient = new(OAuth) + +func ProvideOAuth( + name string, cfg *setting.Cfg, oauthCfg *social.OAuthInfo, + connector social.SocialConnector, httpClient *http.Client, +) *OAuth { + return &OAuth{ + name, fmt.Sprintf("oauth_%s", strings.TrimPrefix(name, "auth.client.")), + log.New(name), cfg, oauthCfg, connector, httpClient, + } +} + +type OAuth struct { + name string + moduleName string + log log.Logger + cfg *setting.Cfg + oauthCfg *social.OAuthInfo + connector social.SocialConnector + httpClient *http.Client +} + +func (c *OAuth) Name() string { + return c.name +} + +func (c *OAuth) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) { + r.SetMeta(authn.MetaKeyAuthModule, c.moduleName) + // get hashed state stored in cookie + stateCookie, err := r.HTTPRequest.Cookie(oauthStateCookieName) + if err != nil { + return nil, errOAuthMissingState.Errorf("missing state cookie") + } + + if stateCookie.Value == "" { + return nil, errOAuthMissingState.Errorf("missing state value in state cookie") + } + + // get state returned by the idp and hash it + stateQuery := hashOAuthState(r.HTTPRequest.URL.Query().Get(oauthStateQueryName), c.cfg.SecretKey, c.oauthCfg.ClientSecret) + // compare the state returned by idp against the one we stored in cookie + if stateQuery != stateCookie.Value { + return nil, errOAuthInvalidState.Errorf("provided state did not match stored state") + } + + var opts []oauth2.AuthCodeOption + // if pkce is enabled for client validate we have the cookie and set it as url param + if c.oauthCfg.UsePKCE { + pkceCookie, err := r.HTTPRequest.Cookie(oauthPKCECookieName) + if err != nil { + return nil, errOAuthMissingPKCE.Errorf("no pkce cookie found: %w", err) + } + opts = append(opts, oauth2.SetAuthURLParam(codeVerifierParamName, pkceCookie.Value)) + } + + clientCtx := context.WithValue(ctx, oauth2.HTTPClient, c.httpClient) + // exchange auth code to a valid token + token, err := c.connector.Exchange(clientCtx, r.HTTPRequest.URL.Query().Get("code"), opts...) + if err != nil { + return nil, err + } + token.TokenType = "Bearer" + + userInfo, err := c.connector.UserInfo(c.connector.Client(clientCtx, token), token) + if err != nil { + return nil, errOAuthTokenExchange.Errorf("failed to exchange code to token: %w", err) + } + + if userInfo.Email == "" { + return nil, errOAuthMissingRequiredEmail.Errorf("required attribute email was not provided") + } + + if !c.connector.IsEmailAllowed(userInfo.Email) { + return nil, errOAuthEmailNotAllowed.Errorf("provided email is not allowed") + } + + return &authn.Identity{ + Login: userInfo.Login, + Name: userInfo.Name, + Email: userInfo.Email, + IsGrafanaAdmin: userInfo.IsGrafanaAdmin, + AuthModule: c.moduleName, + AuthID: userInfo.Id, + Groups: userInfo.Groups, + OAuthToken: token, + OrgRoles: getOAuthOrgRole(userInfo, c.cfg), + ClientParams: authn.ClientParams{ + SyncUser: true, + SyncTeamMembers: true, + AllowSignUp: c.connector.IsSignupAllowed(), + LookUpParams: login.UserLookupParams{Email: &userInfo.Email}, + }, + }, nil +} + +func (c *OAuth) RedirectURL(ctx context.Context, r *authn.Request) (*authn.Redirect, error) { + var opts []oauth2.AuthCodeOption + + if c.oauthCfg.HostedDomain != "" { + opts = append(opts, oauth2.SetAuthURLParam(hostedDomainParamName, c.oauthCfg.HostedDomain)) + } + + var plainPKCE string + if c.oauthCfg.UsePKCE { + pkce, hashedPKCE, err := genPKCECode() + if err != nil { + return nil, errOAuthGenPKCE.Errorf("failed to generate pkce: %w", err) + } + + plainPKCE = pkce + opts = append(opts, + oauth2.SetAuthURLParam(codeChallengeParamName, hashedPKCE), + oauth2.SetAuthURLParam(codeChallengeMethodParamName, codeChallengeMethod), + ) + } + + state, hashedSate, err := genOAuthState(c.cfg.SecretKey, c.oauthCfg.ClientSecret) + if err != nil { + return nil, errOAuthGenState.Errorf("failed to generate state: %w", err) + } + + return &authn.Redirect{ + URL: c.connector.AuthCodeURL(state, opts...), + Extra: map[string]string{ + authn.KeyOAuthState: hashedSate, + authn.KeyOAuthPKCE: plainPKCE, + }, + }, nil +} + +// genPKCECode returns a random URL-friendly string and it's base64 URL encoded SHA256 digest. +func genPKCECode() (string, string, error) { + // IETF RFC 7636 specifies that the code verifier should be 43-128 + // characters from a set of unreserved URI characters which is + // almost the same as the set of characters in base64url. + // https://datatracker.ietf.org/doc/html/rfc7636#section-4.1 + // + // It doesn't hurt to generate a few more bytes here, we generate + // 96 bytes which we then encode using base64url to make sure + // they're within the set of unreserved characters. + // + // 96 is chosen because 96*8/6 = 128, which means that we'll have + // 128 characters after it has been base64 encoded. + raw := make([]byte, 96) + _, err := rand.Read(raw) + if err != nil { + return "", "", err + } + ascii := make([]byte, 128) + base64.RawURLEncoding.Encode(ascii, raw) + + shasum := sha256.Sum256(ascii) + pkce := base64.RawURLEncoding.EncodeToString(shasum[:]) + return string(ascii), pkce, nil +} + +func genOAuthState(secret, seed string) (string, string, error) { + rnd := make([]byte, 32) + if _, err := rand.Read(rnd); err != nil { + return "", "", err + } + state := base64.URLEncoding.EncodeToString(rnd) + return state, hashOAuthState(state, secret, seed), nil +} + +func hashOAuthState(state, secret, seed string) string { + hashBytes := sha256.Sum256([]byte(state + secret + seed)) + return hex.EncodeToString(hashBytes[:]) +} + +func getOAuthOrgRole(userInfo *social.BasicUserInfo, cfg *setting.Cfg) map[int64]org.RoleType { + orgRoles := make(map[int64]org.RoleType, 0) + if cfg.OAuthSkipOrgRoleUpdateSync { + return orgRoles + } + + if userInfo.Role == "" || !userInfo.Role.IsValid() { + return orgRoles + } + + orgID := int64(1) + if cfg.AutoAssignOrg && cfg.AutoAssignOrgId > 0 { + orgID = int64(cfg.AutoAssignOrgId) + } + + orgRoles[orgID] = userInfo.Role + return orgRoles +} diff --git a/pkg/services/authn/clients/oauth_test.go b/pkg/services/authn/clients/oauth_test.go new file mode 100644 index 00000000000..b4f5469d23c --- /dev/null +++ b/pkg/services/authn/clients/oauth_test.go @@ -0,0 +1,305 @@ +package clients + +import ( + "context" + "net/http" + "net/url" + "testing" + + "golang.org/x/oauth2" + + "github.com/grafana/grafana/pkg/login/social" + "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/services/login" + "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/setting" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestOAuth_Authenticate(t *testing.T) { + type testCase struct { + desc string + req *authn.Request + oauthCfg *social.OAuthInfo + + addStateCookie bool + stateCookieValue string + + addPKCECookie bool + pkceCookieValue string + + isEmailAllowed bool + userInfo *social.BasicUserInfo + + expectedErr error + expectedIdentity *authn.Identity + } + + tests := []testCase{ + { + desc: "should return error when missing state cookie", + req: &authn.Request{HTTPRequest: &http.Request{Header: map[string][]string{}}}, + oauthCfg: &social.OAuthInfo{}, + expectedErr: errOAuthMissingState, + }, + { + desc: "should return error when state cookie is present but don't have a value", + req: &authn.Request{HTTPRequest: &http.Request{Header: map[string][]string{}}}, + oauthCfg: &social.OAuthInfo{}, + addStateCookie: true, + stateCookieValue: "", + expectedErr: errOAuthMissingState, + }, + { + desc: "should return error when state from ipd does not match stored state", + req: &authn.Request{HTTPRequest: &http.Request{ + Header: map[string][]string{}, + URL: mustParseURL("http://grafana.com/?state=some-other-state"), + }, + }, + oauthCfg: &social.OAuthInfo{UsePKCE: true}, + addStateCookie: true, + stateCookieValue: "some-state", + expectedErr: errOAuthInvalidState, + }, + { + desc: "should return error when pkce is configured but the cookie is not present", + req: &authn.Request{HTTPRequest: &http.Request{ + Header: map[string][]string{}, + URL: mustParseURL("http://grafana.com/?state=some-state"), + }, + }, + oauthCfg: &social.OAuthInfo{UsePKCE: true}, + addStateCookie: true, + stateCookieValue: "some-state", + expectedErr: errOAuthMissingPKCE, + }, + { + desc: "should return error when email is empty", + req: &authn.Request{HTTPRequest: &http.Request{ + Header: map[string][]string{}, + URL: mustParseURL("http://grafana.com/?state=some-state"), + }, + }, + oauthCfg: &social.OAuthInfo{UsePKCE: true}, + addStateCookie: true, + stateCookieValue: "some-state", + addPKCECookie: true, + pkceCookieValue: "some-pkce-value", + userInfo: &social.BasicUserInfo{}, + expectedErr: errOAuthMissingRequiredEmail, + }, + { + desc: "should return error when email is not allowed", + req: &authn.Request{HTTPRequest: &http.Request{ + Header: map[string][]string{}, + URL: mustParseURL("http://grafana.com/?state=some-state"), + }, + }, + oauthCfg: &social.OAuthInfo{UsePKCE: true}, + addStateCookie: true, + stateCookieValue: "some-state", + addPKCECookie: true, + pkceCookieValue: "some-pkce-value", + userInfo: &social.BasicUserInfo{Email: "some@email.com"}, + isEmailAllowed: false, + expectedErr: errOAuthEmailNotAllowed, + }, + { + desc: "should return identity for valid request", + req: &authn.Request{HTTPRequest: &http.Request{ + Header: map[string][]string{}, + URL: mustParseURL("http://grafana.com/?state=some-state"), + }, + }, + oauthCfg: &social.OAuthInfo{UsePKCE: true}, + addStateCookie: true, + stateCookieValue: "some-state", + addPKCECookie: true, + pkceCookieValue: "some-pkce-value", + isEmailAllowed: true, + userInfo: &social.BasicUserInfo{ + Id: "123", + Name: "name", + Email: "some@email.com", + Role: "Admin", + Groups: []string{"grp1", "grp2"}, + }, + expectedIdentity: &authn.Identity{ + Email: "some@email.com", + AuthModule: "oauth_azuread", + AuthID: "123", + Name: "name", + Groups: []string{"grp1", "grp2"}, + OAuthToken: &oauth2.Token{}, + OrgRoles: map[int64]org.RoleType{1: org.RoleAdmin}, + ClientParams: authn.ClientParams{ + SyncUser: true, + SyncTeamMembers: true, + AllowSignUp: true, + LookUpParams: login.UserLookupParams{Email: strPtr("some@email.com")}, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + cfg := setting.NewCfg() + + if tt.addStateCookie { + v := tt.stateCookieValue + if v != "" { + v = hashOAuthState(v, cfg.SecretKey, tt.oauthCfg.ClientSecret) + } + tt.req.HTTPRequest.AddCookie(&http.Cookie{Name: oauthStateCookieName, Value: v}) + } + + if tt.addPKCECookie { + tt.req.HTTPRequest.AddCookie(&http.Cookie{Name: oauthPKCECookieName, Value: tt.pkceCookieValue}) + } + + c := ProvideOAuth(authn.ClientWithPrefix("azuread"), cfg, tt.oauthCfg, fakeConnector{ + ExpectedUserInfo: tt.userInfo, + ExpectedToken: &oauth2.Token{}, + ExpectedIsSignupAllowed: true, + ExpectedIsEmailAllowed: tt.isEmailAllowed, + }, nil) + identity, err := c.Authenticate(context.Background(), tt.req) + assert.ErrorIs(t, err, tt.expectedErr) + + if tt.expectedIdentity != nil { + assert.Equal(t, tt.expectedIdentity.Login, identity.Login) + assert.Equal(t, tt.expectedIdentity.Name, identity.Name) + assert.Equal(t, tt.expectedIdentity.Email, identity.Email) + assert.Equal(t, tt.expectedIdentity.AuthID, identity.AuthID) + assert.Equal(t, tt.expectedIdentity.AuthModule, identity.AuthModule) + assert.Equal(t, tt.expectedIdentity.Groups, identity.Groups) + + assert.Equal(t, tt.expectedIdentity.ClientParams.SyncUser, identity.ClientParams.SyncUser) + assert.Equal(t, tt.expectedIdentity.ClientParams.AllowSignUp, identity.ClientParams.AllowSignUp) + assert.Equal(t, tt.expectedIdentity.ClientParams.SyncTeamMembers, identity.ClientParams.SyncTeamMembers) + assert.Equal(t, tt.expectedIdentity.ClientParams.EnableDisabledUsers, identity.ClientParams.EnableDisabledUsers) + + assert.EqualValues(t, tt.expectedIdentity.ClientParams.LookUpParams.Email, identity.ClientParams.LookUpParams.Email) + assert.EqualValues(t, tt.expectedIdentity.ClientParams.LookUpParams.Login, identity.ClientParams.LookUpParams.Login) + assert.EqualValues(t, tt.expectedIdentity.ClientParams.LookUpParams.UserID, identity.ClientParams.LookUpParams.UserID) + } else { + assert.Nil(t, tt.expectedIdentity) + } + }) + } +} + +func TestOAuth_RedirectURL(t *testing.T) { + type testCase struct { + desc string + oauthCfg *social.OAuthInfo + expectedErr error + + numCallOptions int + authCodeUrlCalled bool + } + + tests := []testCase{ + { + desc: "should generate redirect url and state", + oauthCfg: &social.OAuthInfo{}, + authCodeUrlCalled: true, + }, + { + desc: "should generate redirect url with hosted domain option if configured", + oauthCfg: &social.OAuthInfo{HostedDomain: "grafana.com"}, + numCallOptions: 1, + authCodeUrlCalled: true, + }, + { + desc: "should generate redirect url with pkce if configured", + oauthCfg: &social.OAuthInfo{UsePKCE: true}, + numCallOptions: 2, + authCodeUrlCalled: true, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + var ( + authCodeUrlCalled = false + ) + + c := ProvideOAuth(authn.ClientWithPrefix("azuread"), setting.NewCfg(), tt.oauthCfg, mockConnector{ + AuthCodeURLFunc: func(state string, opts ...oauth2.AuthCodeOption) string { + authCodeUrlCalled = true + require.Len(t, opts, tt.numCallOptions) + return "" + }, + }, nil) + + redirect, err := c.RedirectURL(context.Background(), nil) + assert.ErrorIs(t, err, tt.expectedErr) + assert.Equal(t, tt.authCodeUrlCalled, authCodeUrlCalled) + + if tt.expectedErr != nil { + return + } + + assert.NotEmpty(t, redirect.Extra[authn.KeyOAuthState]) + if tt.oauthCfg.UsePKCE { + assert.NotEmpty(t, redirect.Extra[authn.KeyOAuthPKCE]) + } + }) + } +} + +type mockConnector struct { + AuthCodeURLFunc func(state string, opts ...oauth2.AuthCodeOption) string + social.SocialConnector +} + +func (m mockConnector) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string { + if m.AuthCodeURLFunc != nil { + return m.AuthCodeURLFunc(state, opts...) + } + return "" +} + +var _ social.SocialConnector = new(fakeConnector) + +type fakeConnector struct { + ExpectedUserInfo *social.BasicUserInfo + ExpectedUserInfoErr error + ExpectedIsEmailAllowed bool + ExpectedIsSignupAllowed bool + ExpectedToken *oauth2.Token + ExpectedTokenErr error + social.SocialConnector +} + +func (f fakeConnector) UserInfo(client *http.Client, token *oauth2.Token) (*social.BasicUserInfo, error) { + return f.ExpectedUserInfo, f.ExpectedUserInfoErr +} + +func (f fakeConnector) IsEmailAllowed(email string) bool { + return f.ExpectedIsEmailAllowed +} + +func (f fakeConnector) IsSignupAllowed() bool { + return f.ExpectedIsSignupAllowed +} + +func (f fakeConnector) Exchange(ctx context.Context, code string, authOptions ...oauth2.AuthCodeOption) (*oauth2.Token, error) { + return f.ExpectedToken, f.ExpectedTokenErr +} + +func (f fakeConnector) Client(ctx context.Context, t *oauth2.Token) *http.Client { + return nil +} + +func mustParseURL(s string) *url.URL { + u, err := url.Parse(s) + if err != nil { + panic(err) + } + return u +} diff --git a/pkg/services/authn/clients/password_test.go b/pkg/services/authn/clients/password_test.go index e064265e90a..85997429041 100644 --- a/pkg/services/authn/clients/password_test.go +++ b/pkg/services/authn/clients/password_test.go @@ -4,11 +4,11 @@ import ( "context" "testing" - "github.com/grafana/grafana/pkg/services/loginattempt/loginattempttest" "github.com/stretchr/testify/assert" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/authn/authntest" + "github.com/grafana/grafana/pkg/services/loginattempt/loginattempttest" ) func TestPassword_AuthenticatePassword(t *testing.T) { diff --git a/pkg/services/authn/clients/proxy_test.go b/pkg/services/authn/clients/proxy_test.go index 62a72e32ff8..e682bb269cf 100644 --- a/pkg/services/authn/clients/proxy_test.go +++ b/pkg/services/authn/clients/proxy_test.go @@ -5,11 +5,11 @@ import ( "net/http" "testing" - "github.com/grafana/grafana/pkg/services/authn/authntest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/services/authn/authntest" "github.com/grafana/grafana/pkg/setting" ) diff --git a/pkg/services/authn/clients/session_test.go b/pkg/services/authn/clients/session_test.go index 65b7bb62326..443f6371a6c 100644 --- a/pkg/services/authn/clients/session_test.go +++ b/pkg/services/authn/clients/session_test.go @@ -7,6 +7,9 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/models/roletype" "github.com/grafana/grafana/pkg/models/usertoken" "github.com/grafana/grafana/pkg/services/auth" @@ -15,8 +18,6 @@ import ( "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/usertest" "github.com/grafana/grafana/pkg/web" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestSession_Test(t *testing.T) { diff --git a/pkg/services/cleanup/cleanup_test.go b/pkg/services/cleanup/cleanup_test.go index 8d92c28d8f4..4db896a207d 100644 --- a/pkg/services/cleanup/cleanup_test.go +++ b/pkg/services/cleanup/cleanup_test.go @@ -4,8 +4,9 @@ import ( "testing" "time" - "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/setting" ) func TestCleanUpTmpFiles(t *testing.T) { diff --git a/pkg/services/comments/sql_storage_test.go b/pkg/services/comments/sql_storage_test.go index 145a2cec5f3..4c8a01a1a24 100644 --- a/pkg/services/comments/sql_storage_test.go +++ b/pkg/services/comments/sql_storage_test.go @@ -5,10 +5,10 @@ import ( "strconv" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/services/comments/commentmodel" - - "github.com/stretchr/testify/require" ) func createSqlStorage(t *testing.T) Storage { diff --git a/pkg/services/contexthandler/auth_jwt.go b/pkg/services/contexthandler/auth_jwt.go index 9944d240bae..a0a5b9750a9 100644 --- a/pkg/services/contexthandler/auth_jwt.go +++ b/pkg/services/contexthandler/auth_jwt.go @@ -9,9 +9,10 @@ import ( "github.com/jmespath/go-jmespath" "github.com/grafana/grafana/pkg/login" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models/roletype" authJWT "github.com/grafana/grafana/pkg/services/auth/jwt" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" + loginsvc "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" ) @@ -22,7 +23,7 @@ const ( UserNotFound = "User not found" ) -func (h *ContextHandler) initContextWithJWT(ctx *models.ReqContext, orgId int64) bool { +func (h *ContextHandler) initContextWithJWT(ctx *contextmodel.ReqContext, orgId int64) bool { if !h.Cfg.JWTAuthEnabled || h.Cfg.JWTAuthHeaderName == "" { return false } @@ -60,10 +61,12 @@ func (h *ContextHandler) initContextWithJWT(ctx *models.ReqContext, orgId int64) ctx.JsonApiErr(http.StatusUnauthorized, InvalidJWT, err) return true } - extUser := &models.ExternalUserInfo{ + extUser := &loginsvc.ExternalUserInfo{ AuthModule: "jwt", AuthId: sub, OrgRoles: map[int64]org.RoleType{}, + // we do not want to sync team memberships from JWT authentication see - https://github.com/grafana/grafana/issues/62175 + SkipTeamSync: true, } if key := h.Cfg.JWTAuthUsernameClaim; key != "" { @@ -114,11 +117,11 @@ func (h *ContextHandler) initContextWithJWT(ctx *models.ReqContext, orgId int64) } if h.Cfg.JWTAuthAutoSignUp { - upsert := &models.UpsertUserCommand{ + upsert := &loginsvc.UpsertUserCommand{ ReqContext: ctx, SignupAllowed: h.Cfg.JWTAuthAutoSignUp, ExternalUser: extUser, - UserLookupParams: models.UserLookupParams{ + UserLookupParams: loginsvc.UserLookupParams{ UserID: nil, Login: &query.Login, Email: &query.Email, diff --git a/pkg/services/contexthandler/auth_proxy_test.go b/pkg/services/contexthandler/auth_proxy_test.go index 2f409ff256d..773e6b36ee0 100644 --- a/pkg/services/contexthandler/auth_proxy_test.go +++ b/pkg/services/contexthandler/auth_proxy_test.go @@ -12,12 +12,13 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/remotecache" "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/auth/authtest" "github.com/grafana/grafana/pkg/services/auth/jwt" "github.com/grafana/grafana/pkg/services/authn/authntest" "github.com/grafana/grafana/pkg/services/contexthandler/authproxy" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/login/loginservice" "github.com/grafana/grafana/pkg/services/org/orgtest" "github.com/grafana/grafana/pkg/services/rendering" @@ -42,7 +43,7 @@ func TestInitContextWithAuthProxy_CachedInvalidUserID(t *testing.T) { req, err := http.NewRequest("POST", "http://example.com", nil) require.NoError(t, err) - ctx := &models.ReqContext{ + ctx := &contextmodel.ReqContext{ Context: &web.Context{Req: req}, Logger: log.New("Test"), } @@ -113,6 +114,6 @@ func getContextHandler(t *testing.T) *ContextHandler { type fakeAuthenticator struct{} -func (fa *fakeAuthenticator) AuthenticateUser(c context.Context, query *models.LoginUserQuery) error { +func (fa *fakeAuthenticator) AuthenticateUser(c context.Context, query *login.LoginUserQuery) error { return nil } diff --git a/pkg/services/contexthandler/authproxy/authproxy.go b/pkg/services/contexthandler/authproxy/authproxy.go index 73c9523d50a..6a6fbda21b7 100644 --- a/pkg/services/contexthandler/authproxy/authproxy.go +++ b/pkg/services/contexthandler/authproxy/authproxy.go @@ -16,7 +16,7 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/remotecache" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/ldap" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/multildap" @@ -98,7 +98,7 @@ func (auth *AuthProxy) IsEnabled() bool { } // HasHeader checks if we have specified header -func (auth *AuthProxy) HasHeader(reqCtx *models.ReqContext) bool { +func (auth *AuthProxy) HasHeader(reqCtx *contextmodel.ReqContext) bool { header := auth.getDecodedHeader(reqCtx, auth.cfg.AuthProxyHeaderName) return len(header) != 0 } @@ -149,7 +149,7 @@ func HashCacheKey(key string) (string, error) { // getKey forms a key for the cache based on the headers received as part of the authentication flow. // Our configuration supports multiple headers. The main header contains the email or username. // And the additional ones that allow us to specify extra attributes: Name, Email, Role, or Groups. -func (auth *AuthProxy) getKey(reqCtx *models.ReqContext) (string, error) { +func (auth *AuthProxy) getKey(reqCtx *contextmodel.ReqContext) (string, error) { header := auth.getDecodedHeader(reqCtx, auth.cfg.AuthProxyHeaderName) key := strings.TrimSpace(header) // start the key with the main header @@ -165,7 +165,7 @@ func (auth *AuthProxy) getKey(reqCtx *models.ReqContext) (string, error) { } // Login logs in user ID by whatever means possible. -func (auth *AuthProxy) Login(reqCtx *models.ReqContext, ignoreCache bool) (int64, error) { +func (auth *AuthProxy) Login(reqCtx *contextmodel.ReqContext, ignoreCache bool) (int64, error) { if !ignoreCache { // Error here means absent cache - we don't need to handle that id, err := auth.getUserViaCache(reqCtx) @@ -195,7 +195,7 @@ func (auth *AuthProxy) Login(reqCtx *models.ReqContext, ignoreCache bool) (int64 } // getUserViaCache gets user ID from cache. -func (auth *AuthProxy) getUserViaCache(reqCtx *models.ReqContext) (int64, error) { +func (auth *AuthProxy) getUserViaCache(reqCtx *contextmodel.ReqContext) (int64, error) { cacheKey, err := auth.getKey(reqCtx) if err != nil { return 0, err @@ -212,7 +212,7 @@ func (auth *AuthProxy) getUserViaCache(reqCtx *models.ReqContext) (int64, error) } // RemoveUserFromCache removes user from cache. -func (auth *AuthProxy) RemoveUserFromCache(reqCtx *models.ReqContext) error { +func (auth *AuthProxy) RemoveUserFromCache(reqCtx *contextmodel.ReqContext) error { cacheKey, err := auth.getKey(reqCtx) if err != nil { return err @@ -227,7 +227,7 @@ func (auth *AuthProxy) RemoveUserFromCache(reqCtx *models.ReqContext) error { } // LoginViaLDAP logs in user via LDAP request -func (auth *AuthProxy) LoginViaLDAP(reqCtx *models.ReqContext) (int64, error) { +func (auth *AuthProxy) LoginViaLDAP(reqCtx *contextmodel.ReqContext) (int64, error) { config, err := getLDAPConfig(auth.cfg) if err != nil { return 0, newError("failed to get LDAP config", err) @@ -241,11 +241,11 @@ func (auth *AuthProxy) LoginViaLDAP(reqCtx *models.ReqContext) (int64, error) { } // Have to sync grafana and LDAP user during log in - upsert := &models.UpsertUserCommand{ + upsert := &login.UpsertUserCommand{ ReqContext: reqCtx, SignupAllowed: auth.cfg.LDAPAllowSignup, ExternalUser: extUser, - UserLookupParams: models.UserLookupParams{ + UserLookupParams: login.UserLookupParams{ Login: &extUser.Login, Email: &extUser.Email, UserID: nil, @@ -259,9 +259,9 @@ func (auth *AuthProxy) LoginViaLDAP(reqCtx *models.ReqContext) (int64, error) { } // loginViaHeader logs in user from the header only -func (auth *AuthProxy) loginViaHeader(reqCtx *models.ReqContext) (int64, error) { +func (auth *AuthProxy) loginViaHeader(reqCtx *contextmodel.ReqContext) (int64, error) { header := auth.getDecodedHeader(reqCtx, auth.cfg.AuthProxyHeaderName) - extUser := &models.ExternalUserInfo{ + extUser := &login.ExternalUserInfo{ AuthModule: login.AuthProxyAuthModule, AuthId: header, } @@ -303,11 +303,11 @@ func (auth *AuthProxy) loginViaHeader(reqCtx *models.ReqContext) (int64, error) } }) - upsert := &models.UpsertUserCommand{ + upsert := &login.UpsertUserCommand{ ReqContext: reqCtx, SignupAllowed: auth.cfg.AuthProxyAutoSignUp, ExternalUser: extUser, - UserLookupParams: models.UserLookupParams{ + UserLookupParams: login.UserLookupParams{ UserID: nil, Login: &extUser.Login, Email: &extUser.Email, @@ -323,7 +323,7 @@ func (auth *AuthProxy) loginViaHeader(reqCtx *models.ReqContext) (int64, error) } // getDecodedHeader gets decoded value of a header with given headerName -func (auth *AuthProxy) getDecodedHeader(reqCtx *models.ReqContext, headerName string) string { +func (auth *AuthProxy) getDecodedHeader(reqCtx *contextmodel.ReqContext, headerName string) string { headerValue := reqCtx.Req.Header.Get(headerName) if auth.cfg.AuthProxyHeadersEncoded { @@ -334,7 +334,7 @@ func (auth *AuthProxy) getDecodedHeader(reqCtx *models.ReqContext, headerName st } // headersIterator iterates over all non-empty supported additional headers -func (auth *AuthProxy) headersIterator(reqCtx *models.ReqContext, fn func(field string, header string)) { +func (auth *AuthProxy) headersIterator(reqCtx *contextmodel.ReqContext, fn func(field string, header string)) { for _, field := range supportedHeaderFields { h := auth.cfg.AuthProxyHeaders[field] if h == "" { @@ -356,7 +356,7 @@ func (auth *AuthProxy) GetSignedInUser(userID int64, orgID int64) (*user.SignedI } // Remember user in cache -func (auth *AuthProxy) Remember(reqCtx *models.ReqContext, id int64) error { +func (auth *AuthProxy) Remember(reqCtx *contextmodel.ReqContext, id int64) error { key, err := auth.getKey(reqCtx) if err != nil { return err diff --git a/pkg/services/contexthandler/authproxy/authproxy_test.go b/pkg/services/contexthandler/authproxy/authproxy_test.go index 736776cc94d..a646a01e68f 100644 --- a/pkg/services/contexthandler/authproxy/authproxy_test.go +++ b/pkg/services/contexthandler/authproxy/authproxy_test.go @@ -11,7 +11,7 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/remotecache" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/ldap" "github.com/grafana/grafana/pkg/services/login/loginservice" "github.com/grafana/grafana/pkg/services/multildap" @@ -23,7 +23,7 @@ import ( const hdrName = "markelog" const id int64 = 42 -func prepareMiddleware(t *testing.T, remoteCache *remotecache.RemoteCache, configureReq func(*http.Request, *setting.Cfg)) (*AuthProxy, *models.ReqContext) { +func prepareMiddleware(t *testing.T, remoteCache *remotecache.RemoteCache, configureReq func(*http.Request, *setting.Cfg)) (*AuthProxy, *contextmodel.ReqContext) { t.Helper() req, err := http.NewRequest("POST", "http://example.com", nil) @@ -38,7 +38,7 @@ func prepareMiddleware(t *testing.T, remoteCache *remotecache.RemoteCache, confi req.Header.Set(cfg.AuthProxyHeaderName, hdrName) } - ctx := &models.ReqContext{ + ctx := &contextmodel.ReqContext{ Context: &web.Context{Req: req}, } diff --git a/pkg/services/contexthandler/contexthandler.go b/pkg/services/contexthandler/contexthandler.go index d9351fca9f4..14f3398ec85 100644 --- a/pkg/services/contexthandler/contexthandler.go +++ b/pkg/services/contexthandler/contexthandler.go @@ -20,13 +20,13 @@ import ( "github.com/grafana/grafana/pkg/infra/tracing" loginpkg "github.com/grafana/grafana/pkg/login" "github.com/grafana/grafana/pkg/middleware/cookies" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/apikey" "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/auth/jwt" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/contexthandler/authproxy" "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/oauthtoken" @@ -99,8 +99,8 @@ type ContextHandler struct { type reqContextKey = ctxkey.Key // FromContext returns the ReqContext value stored in a context.Context, if any. -func FromContext(c context.Context) *models.ReqContext { - if reqCtx, ok := c.Value(reqContextKey{}).(*models.ReqContext); ok { +func FromContext(c context.Context) *contextmodel.ReqContext { + if reqCtx, ok := c.Value(reqContextKey{}).(*contextmodel.ReqContext); ok { return reqCtx } return nil @@ -114,7 +114,7 @@ func (h *ContextHandler) Middleware(next http.Handler) http.Handler { _, span := h.tracer.Start(ctx, "Auth - Middleware") defer span.End() - reqContext := &models.ReqContext{ + reqContext := &contextmodel.ReqContext{ Context: mContext, SignedInUser: &user.SignedInUser{}, IsSignedIn: false, @@ -218,7 +218,7 @@ func (h *ContextHandler) Middleware(next http.Handler) http.Handler { }) } -func (h *ContextHandler) initContextWithAnonymousUser(reqContext *models.ReqContext) bool { +func (h *ContextHandler) initContextWithAnonymousUser(reqContext *contextmodel.ReqContext) bool { _, span := h.tracer.Start(reqContext.Req.Context(), "initContextWithAnonymousUser") defer span.End() @@ -282,7 +282,7 @@ func (h *ContextHandler) getAPIKey(ctx context.Context, keyString string) (*apik return keyQuery.Result, nil } -func (h *ContextHandler) initContextWithAPIKey(reqContext *models.ReqContext) bool { +func (h *ContextHandler) initContextWithAPIKey(reqContext *contextmodel.ReqContext) bool { header := reqContext.Req.Header.Get("Authorization") parts := strings.SplitN(header, " ", 2) var keyString string @@ -396,7 +396,7 @@ func (h *ContextHandler) initContextWithAPIKey(reqContext *models.ReqContext) bo return true } -func (h *ContextHandler) initContextWithBasicAuth(reqContext *models.ReqContext, orgID int64) bool { +func (h *ContextHandler) initContextWithBasicAuth(reqContext *contextmodel.ReqContext, orgID int64) bool { if !h.Cfg.BasicAuthEnabled { return false } @@ -418,7 +418,7 @@ func (h *ContextHandler) initContextWithBasicAuth(reqContext *models.ReqContext, ctx := WithAuthHTTPHeader(reqContext.Req.Context(), "Authorization") *reqContext.Req = *reqContext.Req.WithContext(ctx) - authQuery := models.LoginUserQuery{ + authQuery := login.LoginUserQuery{ Username: username, Password: password, Cfg: h.Cfg, @@ -456,7 +456,7 @@ func (h *ContextHandler) initContextWithBasicAuth(reqContext *models.ReqContext, return true } -func (h *ContextHandler) initContextWithToken(reqContext *models.ReqContext, orgID int64) bool { +func (h *ContextHandler) initContextWithToken(reqContext *contextmodel.ReqContext, orgID int64) bool { if h.Cfg.LoginCookieName == "" { return false } @@ -528,7 +528,7 @@ func (h *ContextHandler) initContextWithToken(reqContext *models.ReqContext, org return true } -func (h *ContextHandler) deleteInvalidCookieEndOfRequestFunc(reqContext *models.ReqContext) web.BeforeFunc { +func (h *ContextHandler) deleteInvalidCookieEndOfRequestFunc(reqContext *contextmodel.ReqContext) web.BeforeFunc { return func(w web.ResponseWriter) { if w.Written() { reqContext.Logger.Debug("Response written, skipping invalid cookie delete") @@ -540,7 +540,7 @@ func (h *ContextHandler) deleteInvalidCookieEndOfRequestFunc(reqContext *models. } } -func (h *ContextHandler) rotateEndOfRequestFunc(reqContext *models.ReqContext) web.BeforeFunc { +func (h *ContextHandler) rotateEndOfRequestFunc(reqContext *contextmodel.ReqContext) web.BeforeFunc { return func(w web.ResponseWriter) { // if response has already been written, skip. if w.Written() { @@ -581,7 +581,7 @@ func (h *ContextHandler) rotateEndOfRequestFunc(reqContext *models.ReqContext) w } } -func (h *ContextHandler) initContextWithRenderAuth(reqContext *models.ReqContext) bool { +func (h *ContextHandler) initContextWithRenderAuth(reqContext *contextmodel.ReqContext) bool { key := reqContext.GetCookie("renderKey") if key == "" { return false @@ -617,7 +617,7 @@ func (h *ContextHandler) initContextWithRenderAuth(reqContext *models.ReqContext return true } -func logUserIn(reqContext *models.ReqContext, auth *authproxy.AuthProxy, username string, logger log.Logger, ignoreCache bool) (int64, error) { +func logUserIn(reqContext *contextmodel.ReqContext, auth *authproxy.AuthProxy, username string, logger log.Logger, ignoreCache bool) (int64, error) { logger.Debug("Trying to log user in", "username", username, "ignoreCache", ignoreCache) // Try to log in user via various providers id, err := auth.Login(reqContext, ignoreCache) @@ -634,7 +634,7 @@ func logUserIn(reqContext *models.ReqContext, auth *authproxy.AuthProxy, usernam return id, nil } -func (h *ContextHandler) handleError(ctx *models.ReqContext, err error, statusCode int, cb func(error)) { +func (h *ContextHandler) handleError(ctx *contextmodel.ReqContext, err error, statusCode int, cb func(error)) { details := err var e authproxy.Error if errors.As(err, &e) { @@ -647,7 +647,7 @@ func (h *ContextHandler) handleError(ctx *models.ReqContext, err error, statusCo } } -func (h *ContextHandler) initContextWithAuthProxy(reqContext *models.ReqContext, orgID int64) bool { +func (h *ContextHandler) initContextWithAuthProxy(reqContext *contextmodel.ReqContext, orgID int64) bool { username := reqContext.Req.Header.Get(h.Cfg.AuthProxyHeaderName) logger := log.New("auth.proxy") @@ -773,7 +773,7 @@ func AuthHTTPHeaderListFromContext(c context.Context) *AuthHTTPHeaderList { return nil } -func (h *ContextHandler) hasAccessTokenExpired(token *models.UserAuth) bool { +func (h *ContextHandler) hasAccessTokenExpired(token *login.UserAuth) bool { if token.OAuthExpiry.IsZero() { return false } diff --git a/pkg/services/contexthandler/contexthandler_test.go b/pkg/services/contexthandler/contexthandler_test.go index a86329cd2ca..efdb5ccb275 100644 --- a/pkg/services/contexthandler/contexthandler_test.go +++ b/pkg/services/contexthandler/contexthandler_test.go @@ -7,14 +7,14 @@ import ( "net/http/httptest" "testing" + "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/auth/authtest" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/web" ) @@ -77,7 +77,7 @@ func TestTokenRotationAtEndOfRequest(t *testing.T) { } func initTokenRotationScenario(ctx context.Context, t *testing.T, ctxHdlr *ContextHandler) ( - *models.ReqContext, *httptest.ResponseRecorder, error) { + *contextmodel.ReqContext, *httptest.ResponseRecorder, error) { t.Helper() ctxHdlr.Cfg.LoginCookieName = "login_token" @@ -92,7 +92,7 @@ func initTokenRotationScenario(ctx context.Context, t *testing.T, ctxHdlr *Conte if err != nil { return nil, nil, err } - reqContext := &models.ReqContext{ + reqContext := &contextmodel.ReqContext{ Context: &web.Context{Req: req}, Logger: log.New("testlogger"), } diff --git a/pkg/models/context.go b/pkg/services/contexthandler/model/model.go similarity index 99% rename from pkg/models/context.go rename to pkg/services/contexthandler/model/model.go index 97763e39b7e..58c874a2193 100644 --- a/pkg/models/context.go +++ b/pkg/services/contexthandler/model/model.go @@ -1,10 +1,12 @@ -package models +package contextmodel import ( "errors" "net/http" "strings" + "github.com/prometheus/client_golang/prometheus" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/models/usertoken" @@ -13,7 +15,6 @@ import ( "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util/errutil" "github.com/grafana/grafana/pkg/web" - "github.com/prometheus/client_golang/prometheus" ) type ReqContext struct { diff --git a/pkg/models/context_test.go b/pkg/services/contexthandler/model/model_test.go similarity index 97% rename from pkg/models/context_test.go rename to pkg/services/contexthandler/model/model_test.go index 37b5b58cc5f..151236e4247 100644 --- a/pkg/models/context_test.go +++ b/pkg/services/contexthandler/model/model_test.go @@ -1,11 +1,12 @@ -package models +package contextmodel import ( "net/http" "testing" - "github.com/grafana/grafana/pkg/web" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/web" ) func TestQueryBoolWithDefault(t *testing.T) { diff --git a/pkg/services/correlations/api.go b/pkg/services/correlations/api.go index aebba66f847..a654d3c9bc7 100644 --- a/pkg/services/correlations/api.go +++ b/pkg/services/correlations/api.go @@ -7,10 +7,9 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/models" ac "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" - "github.com/grafana/grafana/pkg/web" ) @@ -43,7 +42,7 @@ func (s *CorrelationsService) registerAPIEndpoints() { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (s *CorrelationsService) createHandler(c *models.ReqContext) response.Response { +func (s *CorrelationsService) createHandler(c *contextmodel.ReqContext) response.Response { cmd := CreateCorrelationCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -93,7 +92,7 @@ type CreateCorrelationResponse struct { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (s *CorrelationsService) deleteHandler(c *models.ReqContext) response.Response { +func (s *CorrelationsService) deleteHandler(c *contextmodel.ReqContext) response.Response { cmd := DeleteCorrelationCommand{ UID: web.Params(c.Req)[":correlationUID"], SourceUID: web.Params(c.Req)[":uid"], @@ -147,7 +146,7 @@ type DeleteCorrelationResponse struct { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (s *CorrelationsService) updateHandler(c *models.ReqContext) response.Response { +func (s *CorrelationsService) updateHandler(c *contextmodel.ReqContext) response.Response { cmd := UpdateCorrelationCommand{} if err := web.Bind(c.Req, &cmd); err != nil { if errors.Is(err, ErrUpdateCorrelationEmptyParams) { @@ -208,7 +207,7 @@ type UpdateCorrelationResponse struct { // 401: unauthorisedError // 404: notFoundError // 500: internalServerError -func (s *CorrelationsService) getCorrelationHandler(c *models.ReqContext) response.Response { +func (s *CorrelationsService) getCorrelationHandler(c *contextmodel.ReqContext) response.Response { query := GetCorrelationQuery{ UID: web.Params(c.Req)[":correlationUID"], SourceUID: web.Params(c.Req)[":uid"], @@ -255,7 +254,7 @@ type GetCorrelationResponse struct { // 401: unauthorisedError // 404: notFoundError // 500: internalServerError -func (s *CorrelationsService) getCorrelationsBySourceUIDHandler(c *models.ReqContext) response.Response { +func (s *CorrelationsService) getCorrelationsBySourceUIDHandler(c *contextmodel.ReqContext) response.Response { query := GetCorrelationsBySourceUIDQuery{ SourceUID: web.Params(c.Req)[":uid"], OrgId: c.OrgID, @@ -298,7 +297,7 @@ type GetCorrelationsBySourceUIDResponse struct { // 401: unauthorisedError // 404: notFoundError // 500: internalServerError -func (s *CorrelationsService) getCorrelationsHandler(c *models.ReqContext) response.Response { +func (s *CorrelationsService) getCorrelationsHandler(c *contextmodel.ReqContext) response.Response { query := GetCorrelationsQuery{ OrgId: c.OrgID, } diff --git a/pkg/services/correlations/correlations.go b/pkg/services/correlations/correlations.go index ed6da7b6fe1..2afdde08361 100644 --- a/pkg/services/correlations/correlations.go +++ b/pkg/services/correlations/correlations.go @@ -6,7 +6,6 @@ import ( "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/events" - "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/accesscontrol" diff --git a/pkg/services/dashboardimport/api/api.go b/pkg/services/dashboardimport/api/api.go index f491d645bdc..24ddb46c9f4 100644 --- a/pkg/services/dashboardimport/api/api.go +++ b/pkg/services/dashboardimport/api/api.go @@ -7,9 +7,9 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboardimport" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/quota" @@ -55,7 +55,7 @@ func (api *ImportDashboardAPI) RegisterAPIEndpoints(routeRegister routing.RouteR // 412: preconditionFailedError // 422: unprocessableEntityError // 500: internalServerError -func (api *ImportDashboardAPI) ImportDashboard(c *models.ReqContext) response.Response { +func (api *ImportDashboardAPI) ImportDashboard(c *contextmodel.ReqContext) response.Response { req := dashboardimport.ImportDashboardRequest{} if err := web.Bind(c.Req, &req); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -84,12 +84,12 @@ func (api *ImportDashboardAPI) ImportDashboard(c *models.ReqContext) response.Re } type QuotaService interface { - QuotaReached(c *models.ReqContext, target quota.TargetSrv) (bool, error) + QuotaReached(c *contextmodel.ReqContext, target quota.TargetSrv) (bool, error) } -type quotaServiceFunc func(c *models.ReqContext, target quota.TargetSrv) (bool, error) +type quotaServiceFunc func(c *contextmodel.ReqContext, target quota.TargetSrv) (bool, error) -func (fn quotaServiceFunc) QuotaReached(c *models.ReqContext, target quota.TargetSrv) (bool, error) { +func (fn quotaServiceFunc) QuotaReached(c *contextmodel.ReqContext, target quota.TargetSrv) (bool, error) { return fn(c, target) } diff --git a/pkg/services/dashboardimport/api/api_test.go b/pkg/services/dashboardimport/api/api_test.go index d688e019109..4752f4c27ff 100644 --- a/pkg/services/dashboardimport/api/api_test.go +++ b/pkg/services/dashboardimport/api/api_test.go @@ -7,15 +7,16 @@ import ( "net/http" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/models" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboardimport" "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/web/webtest" - "github.com/stretchr/testify/require" ) func TestImportDashboardAPI(t *testing.T) { @@ -166,10 +167,10 @@ func (s *serviceMock) ImportDashboard(ctx context.Context, req *dashboardimport. return nil, nil } -func quotaReached(c *models.ReqContext, target quota.TargetSrv) (bool, error) { +func quotaReached(c *contextmodel.ReqContext, target quota.TargetSrv) (bool, error) { return true, nil } -func quotaNotReached(c *models.ReqContext, target quota.TargetSrv) (bool, error) { +func quotaNotReached(c *contextmodel.ReqContext, target quota.TargetSrv) (bool, error) { return false, nil } diff --git a/pkg/services/dashboardimport/service/service_test.go b/pkg/services/dashboardimport/service/service_test.go index f3086f2a863..c0c14d601e2 100644 --- a/pkg/services/dashboardimport/service/service_test.go +++ b/pkg/services/dashboardimport/service/service_test.go @@ -6,6 +6,8 @@ import ( "path/filepath" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/services/dashboardimport" "github.com/grafana/grafana/pkg/services/dashboards" @@ -15,7 +17,6 @@ import ( "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/plugindashboards" "github.com/grafana/grafana/pkg/services/user" - "github.com/stretchr/testify/require" ) func TestImportDashboardService(t *testing.T) { diff --git a/pkg/services/dashboardimport/utils/dash_template_evaluator_test.go b/pkg/services/dashboardimport/utils/dash_template_evaluator_test.go index 9d6053228a4..0fe2a3d09a5 100644 --- a/pkg/services/dashboardimport/utils/dash_template_evaluator_test.go +++ b/pkg/services/dashboardimport/utils/dash_template_evaluator_test.go @@ -3,9 +3,10 @@ package utils import ( "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/services/dashboardimport" - "github.com/stretchr/testify/require" ) func TestDashTemplateEvaluator(t *testing.T) { diff --git a/pkg/services/dashboards/accesscontrol.go b/pkg/services/dashboards/accesscontrol.go index 3c401ada3e1..16ea72b2d59 100644 --- a/pkg/services/dashboards/accesscontrol.go +++ b/pkg/services/dashboards/accesscontrol.go @@ -54,7 +54,7 @@ func NewFolderNameScopeResolver(db Store, folderDB FolderStore, folderSvc folder return nil, err } - result, err := getInheritedScopes(ctx, folder.OrgID, folder.UID, folderSvc) + result, err := GetInheritedScopes(ctx, folder.OrgID, folder.UID, folderSvc) if err != nil { return nil, err } @@ -86,7 +86,7 @@ func NewFolderIDScopeResolver(db Store, folderDB FolderStore, folderSvc folder.S return nil, err } - result, err := getInheritedScopes(ctx, folder.OrgID, folder.UID, folderSvc) + result, err := GetInheritedScopes(ctx, folder.OrgID, folder.UID, folderSvc) if err != nil { return nil, err } @@ -158,7 +158,7 @@ func resolveDashboardScope(ctx context.Context, db Store, folderDB FolderStore, folderUID = folder.UID } - result, err := getInheritedScopes(ctx, orgID, folderUID, folderSvc) + result, err := GetInheritedScopes(ctx, orgID, folderUID, folderSvc) if err != nil { return nil, err } @@ -173,15 +173,14 @@ func resolveDashboardScope(ctx context.Context, db Store, folderDB FolderStore, return result, nil } -func getInheritedScopes(ctx context.Context, orgID int64, folderUID string, folderSvc folder.Service) ([]string, error) { +func GetInheritedScopes(ctx context.Context, orgID int64, folderUID string, folderSvc folder.Service) ([]string, error) { ancestors, err := folderSvc.GetParents(ctx, folder.GetParentsQuery{ UID: folderUID, OrgID: orgID, }) if err != nil { - // TODO return a specific error - return nil, err + return nil, ac.ErrInternal.Errorf("could not retrieve folder parents: %w", err) } result := make([]string, 0, len(ancestors)) diff --git a/pkg/services/dashboards/dashboard_service_mock.go b/pkg/services/dashboards/dashboard_service_mock.go index 3da367b22be..a59d811dbbe 100644 --- a/pkg/services/dashboards/dashboard_service_mock.go +++ b/pkg/services/dashboards/dashboard_service_mock.go @@ -5,8 +5,9 @@ package dashboards import ( context "context" - folder "github.com/grafana/grafana/pkg/services/folder" mock "github.com/stretchr/testify/mock" + + folder "github.com/grafana/grafana/pkg/services/folder" ) // FakeDashboardService is an autogenerated mock type for the DashboardService type diff --git a/pkg/services/dashboards/database/database_test.go b/pkg/services/dashboards/database/database_test.go index 7f643a8fc78..eb5a591ea6f 100644 --- a/pkg/services/dashboards/database/database_test.go +++ b/pkg/services/dashboards/database/database_test.go @@ -13,12 +13,12 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/publicdashboards/database" publicDashboardModels "github.com/grafana/grafana/pkg/services/publicdashboards/models" "github.com/grafana/grafana/pkg/services/quota/quotatest" + "github.com/grafana/grafana/pkg/services/search/model" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore/searchstore" "github.com/grafana/grafana/pkg/services/star" @@ -407,7 +407,7 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { require.Equal(t, len(query.Result), 1) hit := query.Result[0] - require.Equal(t, hit.Type, models.DashHitFolder) + require.Equal(t, hit.Type, model.DashHitFolder) require.Equal(t, hit.URL, fmt.Sprintf("/dashboards/f/%s/%s", savedFolder.UID, savedFolder.Slug)) require.Equal(t, hit.FolderTitle, "") }) @@ -648,8 +648,8 @@ func TestIntegrationDashboard_SortingOptions(t *testing.T) { 1: {dashboards.ActionDashboardsRead: []string{dashboards.ScopeDashboardsAll}}, }, }, - Sort: models.SortOption{ - Filter: []models.SortOptionFilter{ + Sort: model.SortOption{ + Filter: []model.SortOptionFilter{ searchstore.TitleSorter{Descending: true}, }, }, @@ -850,18 +850,18 @@ func testSearchDashboards(d *DashboardStore, query *dashboards.FindPersistedDash } func makeQueryResult(query *dashboards.FindPersistedDashboardsQuery, res []dashboards.DashboardSearchProjection) { - query.Result = make([]*models.Hit, 0) - hits := make(map[int64]*models.Hit) + query.Result = make([]*model.Hit, 0) + hits := make(map[int64]*model.Hit) for _, item := range res { hit, exists := hits[item.ID] if !exists { - hitType := models.DashHitDB + hitType := model.DashHitDB if item.IsFolder { - hitType = models.DashHitFolder + hitType = model.DashHitFolder } - hit = &models.Hit{ + hit = &model.Hit{ ID: item.ID, UID: item.UID, Title: item.Title, diff --git a/pkg/services/dashboards/folder_store_mock.go b/pkg/services/dashboards/folder_store_mock.go index df548e85efb..47fdf2baa87 100644 --- a/pkg/services/dashboards/folder_store_mock.go +++ b/pkg/services/dashboards/folder_store_mock.go @@ -5,8 +5,9 @@ package dashboards import ( context "context" - folder "github.com/grafana/grafana/pkg/services/folder" mock "github.com/stretchr/testify/mock" + + folder "github.com/grafana/grafana/pkg/services/folder" ) // FakeFolderStore is an autogenerated mock type for the FolderStore type diff --git a/pkg/services/dashboards/models.go b/pkg/services/dashboards/models.go index b67cd20c33c..f9f3fcb2f2c 100644 --- a/pkg/services/dashboards/models.go +++ b/pkg/services/dashboards/models.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/quota" + "github.com/grafana/grafana/pkg/services/search/model" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" ) @@ -412,9 +413,9 @@ type FindPersistedDashboardsQuery struct { Limit int64 Page int64 Permission PermissionType - Sort models.SortOption + Sort model.SortOption Filters []interface{} - Result models.HitList + Result model.HitList } diff --git a/pkg/services/dashboards/models_test.go b/pkg/services/dashboards/models_test.go index 9a6299997a4..b7ba915324b 100644 --- a/pkg/services/dashboards/models_test.go +++ b/pkg/services/dashboards/models_test.go @@ -3,11 +3,12 @@ package dashboards import ( "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/slugify" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestGetDashboardUrl(t *testing.T) { diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index b4098316e02..ca46e7beb35 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -10,7 +10,6 @@ import ( "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/dashboards" @@ -18,6 +17,7 @@ import ( "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/search/model" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" @@ -553,25 +553,25 @@ func (dr *DashboardServiceImpl) SearchDashboards(ctx context.Context, query *das return nil } -func getHitType(item dashboards.DashboardSearchProjection) models.HitType { - var hitType models.HitType +func getHitType(item dashboards.DashboardSearchProjection) model.HitType { + var hitType model.HitType if item.IsFolder { - hitType = models.DashHitFolder + hitType = model.DashHitFolder } else { - hitType = models.DashHitDB + hitType = model.DashHitDB } return hitType } func makeQueryResult(query *dashboards.FindPersistedDashboardsQuery, res []dashboards.DashboardSearchProjection) { - query.Result = make([]*models.Hit, 0) - hits := make(map[int64]*models.Hit) + query.Result = make([]*model.Hit, 0) + hits := make(map[int64]*model.Hit) for _, item := range res { hit, exists := hits[item.ID] if !exists { - hit = &models.Hit{ + hit = &model.Hit{ ID: item.ID, UID: item.UID, Title: item.Title, diff --git a/pkg/services/dashboards/store_mock.go b/pkg/services/dashboards/store_mock.go index f32268a86ce..4b2b5a9bded 100644 --- a/pkg/services/dashboards/store_mock.go +++ b/pkg/services/dashboards/store_mock.go @@ -5,11 +5,10 @@ package dashboards import ( context "context" - folder "github.com/grafana/grafana/pkg/services/folder" mock "github.com/stretchr/testify/mock" models "github.com/grafana/grafana/pkg/services/alerting/models" - + folder "github.com/grafana/grafana/pkg/services/folder" quota "github.com/grafana/grafana/pkg/services/quota" ) diff --git a/pkg/services/datasourceproxy/datasourceproxy.go b/pkg/services/datasourceproxy/datasourceproxy.go index c6346fa9682..de382029364 100644 --- a/pkg/services/datasourceproxy/datasourceproxy.go +++ b/pkg/services/datasourceproxy/datasourceproxy.go @@ -12,8 +12,8 @@ import ( "github.com/grafana/grafana/pkg/infra/httpclient" "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/oauthtoken" "github.com/grafana/grafana/pkg/services/secrets" @@ -52,7 +52,7 @@ type DataSourceProxyService struct { secretsService secrets.Service } -func (p *DataSourceProxyService) ProxyDataSourceRequest(c *models.ReqContext) { +func (p *DataSourceProxyService) ProxyDataSourceRequest(c *contextmodel.ReqContext) { id, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64) if err != nil { c.JsonApiErr(http.StatusBadRequest, "id is invalid", err) @@ -61,7 +61,7 @@ func (p *DataSourceProxyService) ProxyDataSourceRequest(c *models.ReqContext) { p.ProxyDatasourceRequestWithID(c, id) } -func (p *DataSourceProxyService) ProxyDatasourceRequestWithUID(c *models.ReqContext, dsUID string) { +func (p *DataSourceProxyService) ProxyDatasourceRequestWithUID(c *contextmodel.ReqContext, dsUID string) { c.TimeRequest(metrics.MDataSourceProxyReqTimer) if dsUID == "" { // if datasource UID is not provided, fetch it from the uid path parameter @@ -81,7 +81,7 @@ func (p *DataSourceProxyService) ProxyDatasourceRequestWithUID(c *models.ReqCont p.proxyDatasourceRequest(c, ds) } -func (p *DataSourceProxyService) ProxyDatasourceRequestWithID(c *models.ReqContext, dsID int64) { +func (p *DataSourceProxyService) ProxyDatasourceRequestWithID(c *contextmodel.ReqContext, dsID int64) { c.TimeRequest(metrics.MDataSourceProxyReqTimer) ds, err := p.DataSourceCache.GetDatasource(c.Req.Context(), dsID, c.SignedInUser, c.SkipCache) @@ -92,7 +92,7 @@ func (p *DataSourceProxyService) ProxyDatasourceRequestWithID(c *models.ReqConte p.proxyDatasourceRequest(c, ds) } -func toAPIError(c *models.ReqContext, err error) { +func toAPIError(c *contextmodel.ReqContext, err error) { if errors.Is(err, datasources.ErrDataSourceAccessDenied) { c.JsonApiErr(http.StatusForbidden, "Access denied to datasource", err) return @@ -104,7 +104,7 @@ func toAPIError(c *models.ReqContext, err error) { c.JsonApiErr(http.StatusInternalServerError, "Unable to load datasource meta data", err) } -func (p *DataSourceProxyService) proxyDatasourceRequest(c *models.ReqContext, ds *datasources.DataSource) { +func (p *DataSourceProxyService) proxyDatasourceRequest(c *contextmodel.ReqContext, ds *datasources.DataSource) { err := p.PluginRequestValidator.Validate(ds.Url, c.Req) if err != nil { c.JsonApiErr(http.StatusForbidden, "Access denied", err) @@ -138,6 +138,6 @@ func extractProxyPath(originalRawPath string) string { return proxyPathRegexp.ReplaceAllString(originalRawPath, "") } -func getProxyPath(c *models.ReqContext) string { +func getProxyPath(c *contextmodel.ReqContext) string { return extractProxyPath(c.Req.URL.EscapedPath()) } diff --git a/pkg/services/encryption/provider/cipher_aescfb_test.go b/pkg/services/encryption/provider/cipher_aescfb_test.go index d717381590f..ff067781ca8 100644 --- a/pkg/services/encryption/provider/cipher_aescfb_test.go +++ b/pkg/services/encryption/provider/cipher_aescfb_test.go @@ -5,7 +5,6 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) diff --git a/pkg/services/encryption/provider/decipher_aes_test.go b/pkg/services/encryption/provider/decipher_aes_test.go index b31958e8886..5dbe9142c66 100644 --- a/pkg/services/encryption/provider/decipher_aes_test.go +++ b/pkg/services/encryption/provider/decipher_aes_test.go @@ -4,9 +4,10 @@ import ( "context" "testing" - "github.com/grafana/grafana/pkg/services/encryption" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/services/encryption" ) func Test_aesDecipher(t *testing.T) { diff --git a/pkg/services/encryption/service/helpers.go b/pkg/services/encryption/service/helpers.go index d32c1600d94..bbd1b5850f1 100644 --- a/pkg/services/encryption/service/helpers.go +++ b/pkg/services/encryption/service/helpers.go @@ -3,10 +3,11 @@ package service import ( "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/usagestats" encryptionprovider "github.com/grafana/grafana/pkg/services/encryption/provider" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/require" ) func SetupTestService(tb testing.TB) *Service { diff --git a/pkg/services/encryption/service/service_test.go b/pkg/services/encryption/service/service_test.go index e4850724699..6e0a58d40d8 100644 --- a/pkg/services/encryption/service/service_test.go +++ b/pkg/services/encryption/service/service_test.go @@ -4,12 +4,13 @@ import ( "context" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/services/encryption" "github.com/grafana/grafana/pkg/services/encryption/provider" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func Test_Service(t *testing.T) { diff --git a/pkg/services/export/service.go b/pkg/services/export/service.go index 3e7f03e14d1..173635c524b 100644 --- a/pkg/services/export/service.go +++ b/pkg/services/export/service.go @@ -14,7 +14,7 @@ import ( "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboardsnapshots" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -27,16 +27,16 @@ import ( type ExportService interface { // List folder contents - HandleGetStatus(c *models.ReqContext) response.Response + HandleGetStatus(c *contextmodel.ReqContext) response.Response // List Get Options - HandleGetOptions(c *models.ReqContext) response.Response + HandleGetOptions(c *contextmodel.ReqContext) response.Response // Read raw file contents out of the store - HandleRequestExport(c *models.ReqContext) response.Response + HandleRequestExport(c *contextmodel.ReqContext) response.Response // Cancel any running export - HandleRequestStop(c *models.ReqContext) response.Response + HandleRequestStop(c *contextmodel.ReqContext) response.Response } var exporters = []Exporter{ @@ -186,21 +186,21 @@ func ProvideService(db db.DB, features featuremgmt.FeatureToggles, gl *live.Graf } } -func (ex *StandardExport) HandleGetOptions(c *models.ReqContext) response.Response { +func (ex *StandardExport) HandleGetOptions(c *contextmodel.ReqContext) response.Response { info := map[string]interface{}{ "exporters": exporters, } return response.JSON(http.StatusOK, info) } -func (ex *StandardExport) HandleGetStatus(c *models.ReqContext) response.Response { +func (ex *StandardExport) HandleGetStatus(c *contextmodel.ReqContext) response.Response { ex.mutex.Lock() defer ex.mutex.Unlock() return response.JSON(http.StatusOK, ex.exportJob.getStatus()) } -func (ex *StandardExport) HandleRequestStop(c *models.ReqContext) response.Response { +func (ex *StandardExport) HandleRequestStop(c *contextmodel.ReqContext) response.Response { ex.mutex.Lock() defer ex.mutex.Unlock() @@ -209,7 +209,7 @@ func (ex *StandardExport) HandleRequestStop(c *models.ReqContext) response.Respo return response.JSON(http.StatusOK, ex.exportJob.getStatus()) } -func (ex *StandardExport) HandleRequestExport(c *models.ReqContext) response.Response { +func (ex *StandardExport) HandleRequestExport(c *contextmodel.ReqContext) response.Response { var cfg ExportConfig err := json.NewDecoder(c.Req.Body).Decode(&cfg) if err != nil { diff --git a/pkg/services/export/stub.go b/pkg/services/export/stub.go index a0ad9a106f8..fe8d9768d6b 100644 --- a/pkg/services/export/stub.go +++ b/pkg/services/export/stub.go @@ -4,25 +4,25 @@ import ( "net/http" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" ) var _ ExportService = new(StubExport) type StubExport struct{} -func (ex *StubExport) HandleGetStatus(c *models.ReqContext) response.Response { +func (ex *StubExport) HandleGetStatus(c *contextmodel.ReqContext) response.Response { return response.Error(http.StatusForbidden, "feature not enabled", nil) } -func (ex *StubExport) HandleGetOptions(c *models.ReqContext) response.Response { +func (ex *StubExport) HandleGetOptions(c *contextmodel.ReqContext) response.Response { return response.Error(http.StatusForbidden, "feature not enabled", nil) } -func (ex *StubExport) HandleRequestExport(c *models.ReqContext) response.Response { +func (ex *StubExport) HandleRequestExport(c *contextmodel.ReqContext) response.Response { return response.Error(http.StatusForbidden, "feature not enabled", nil) } -func (ex *StubExport) HandleRequestStop(c *models.ReqContext) response.Response { +func (ex *StubExport) HandleRequestStop(c *contextmodel.ReqContext) response.Response { return response.Error(http.StatusForbidden, "feature not enabled", nil) } diff --git a/pkg/services/featuremgmt/manager.go b/pkg/services/featuremgmt/manager.go index 19a9f91c6e3..223393fde8f 100644 --- a/pkg/services/featuremgmt/manager.go +++ b/pkg/services/featuremgmt/manager.go @@ -8,7 +8,7 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/licensing" ) @@ -151,7 +151,7 @@ func (fm *FeatureManager) GetFlags() []FeatureFlag { return v } -func (fm *FeatureManager) HandleGetSettings(c *models.ReqContext) { +func (fm *FeatureManager) HandleGetSettings(c *contextmodel.ReqContext) { res := make(map[string]interface{}, 3) res["enabled"] = fm.GetEnabled(c.Req.Context()) diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 46df1ffbd58..4910a8c9d3c 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -177,13 +177,6 @@ var ( State: FeatureStateAlpha, RequiresDevMode: true, }, - { - Name: "azureMonitorResourcePickerForMetrics", - Description: "New UI for Azure Monitor Metrics Query", - State: FeatureStateAlpha, - RequiresDevMode: true, - FrontendOnly: true, - }, { Name: "exploreMixedDatasource", Description: "Enable mixed datasource in Explore", @@ -305,8 +298,9 @@ var ( }, { Name: "cloudWatchCrossAccountQuerying", - Description: "Use cross-account querying in CloudWatch datasource", - State: FeatureStateAlpha, + Description: "Enables cross-account querying in CloudWatch datasources", + State: FeatureStateStable, + Expression: "true", //enabled by default }, { Name: "redshiftAsyncQueryDataSupport", @@ -415,16 +409,25 @@ var ( State: FeatureStateBeta, RequiresRestart: false, }, - { - Name: "azureMultipleResourcePicker", - Description: "Azure multiple resource picker", - State: FeatureStateAlpha, - }, { Name: "topNavCommandPalette", Description: "Launch the Command Palette from the top navigation search box", State: FeatureStateBeta, FrontendOnly: true, }, + { + + Name: "logsSampleInExplore", + Description: "Enables access to the logs sample feature in Explore", + State: FeatureStateStable, + Expression: "true", //turned on by default + FrontendOnly: true, + }, + { + Name: "logsContextDatasourceUi", + Description: "Allow datasource to provide custom UI for context view", + State: FeatureStateAlpha, + FrontendOnly: true, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 825eba4df39..7301eb570ba 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -131,10 +131,6 @@ const ( // Export grafana instance (to git, etc) FlagExport = "export" - // FlagAzureMonitorResourcePickerForMetrics - // New UI for Azure Monitor Metrics Query - FlagAzureMonitorResourcePickerForMetrics = "azureMonitorResourcePickerForMetrics" - // FlagExploreMixedDatasource // Enable mixed datasource in Explore FlagExploreMixedDatasource = "exploreMixedDatasource" @@ -220,7 +216,7 @@ const ( FlagFlameGraph = "flameGraph" // FlagCloudWatchCrossAccountQuerying - // Use cross-account querying in CloudWatch datasource + // Enables cross-account querying in CloudWatch datasources FlagCloudWatchCrossAccountQuerying = "cloudWatchCrossAccountQuerying" // FlagRedshiftAsyncQueryDataSupport @@ -303,11 +299,15 @@ const ( // Stop maintaining state of alerts that are not firing FlagAlertingNoNormalState = "alertingNoNormalState" - // FlagAzureMultipleResourcePicker - // Azure multiple resource picker - FlagAzureMultipleResourcePicker = "azureMultipleResourcePicker" - // FlagTopNavCommandPalette // Launch the Command Palette from the top navigation search box FlagTopNavCommandPalette = "topNavCommandPalette" + + // FlagLogsSampleInExplore + // Enables access to the logs sample feature in Explore + FlagLogsSampleInExplore = "logsSampleInExplore" + + // FlagLogsContextDatasourceUi + // Allow datasource to provide custom UI for context view + FlagLogsContextDatasourceUi = "logsContextDatasourceUi" ) diff --git a/pkg/services/folder/folderimpl/folder.go b/pkg/services/folder/folderimpl/folder.go index f7dfb2e3266..e9da3fa4cec 100644 --- a/pkg/services/folder/folderimpl/folder.go +++ b/pkg/services/folder/folderimpl/folder.go @@ -15,14 +15,13 @@ import ( "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" - "github.com/grafana/grafana/pkg/services/sqlstore" - "github.com/grafana/grafana/pkg/services/sqlstore/migrator" - "github.com/grafana/grafana/pkg/util" - "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util" ) type Service struct { @@ -33,7 +32,6 @@ type Service struct { dashboardStore dashboards.Store dashboardFolderStore dashboards.FolderStore features featuremgmt.FeatureToggles - permissions accesscontrol.FolderPermissionsService accessControl accesscontrol.AccessControl // bus is currently used to publish events that cause scheduler to update rules. @@ -48,7 +46,6 @@ func ProvideService( folderStore dashboards.FolderStore, db db.DB, // DB for the (new) nested folder store features featuremgmt.FeatureToggles, - folderPermissionsService accesscontrol.FolderPermissionsService, ) folder.Service { store := ProvideStore(db, cfg, features) svr := &Service{ @@ -58,7 +55,6 @@ func ProvideService( dashboardFolderStore: folderStore, store: store, features: features, - permissions: folderPermissionsService, accessControl: ac, bus: bus, } @@ -312,29 +308,6 @@ func (s *Service) Create(ctx context.Context, cmd *folder.CreateFolderCommand) ( return nil, err } - var permissionErr error - if !accesscontrol.IsDisabled(s.cfg) { - var permissions []accesscontrol.SetResourcePermissionCommand - if user.IsRealUser() && !user.IsAnonymous { - permissions = append(permissions, accesscontrol.SetResourcePermissionCommand{ - UserID: userID, Permission: dashboards.PERMISSION_ADMIN.String(), - }) - } - - permissions = append(permissions, []accesscontrol.SetResourcePermissionCommand{ - {BuiltinRole: string(org.RoleEditor), Permission: dashboards.PERMISSION_EDIT.String()}, - {BuiltinRole: string(org.RoleViewer), Permission: dashboards.PERMISSION_VIEW.String()}, - }...) - - _, permissionErr = s.permissions.SetPermissions(ctx, cmd.OrgID, createdFolder.UID, permissions...) - } else if s.cfg.EditorsCanAdmin && user.IsRealUser() && !user.IsAnonymous { - permissionErr = s.MakeUserAdmin(ctx, cmd.OrgID, userID, createdFolder.ID, true) - } - - if permissionErr != nil { - logger.Error("Could not make user admin", "folder", createdFolder.Title, "user", userID, "error", permissionErr) - } - var nestedFolder *folder.Folder if s.features.IsEnabled(featuremgmt.FlagNestedFolders) { cmd := &folder.CreateFolderCommand{ diff --git a/pkg/services/folder/folderimpl/folder_test.go b/pkg/services/folder/folderimpl/folder_test.go index 948178d3ace..3f989943f06 100644 --- a/pkg/services/folder/folderimpl/folder_test.go +++ b/pkg/services/folder/folderimpl/folder_test.go @@ -37,7 +37,7 @@ func TestIntegrationProvideFolderService(t *testing.T) { t.Run("should register scope resolvers", func(t *testing.T) { cfg := setting.NewCfg() ac := acmock.New() - ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), cfg, nil, nil, nil, &featuremgmt.FeatureManager{}, nil) + ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), cfg, nil, nil, nil, &featuremgmt.FeatureManager{}) require.Len(t, ac.Calls.RegisterAttributeScopeResolver, 2) }) @@ -57,7 +57,6 @@ func TestIntegrationFolderService(t *testing.T) { cfg := setting.NewCfg() cfg.RBACEnabled = false features := featuremgmt.WithFeatures() - folderPermissions := acmock.NewMockedPermissionsService() service := &Service{ cfg: cfg, @@ -66,7 +65,6 @@ func TestIntegrationFolderService(t *testing.T) { dashboardFolderStore: folderStore, store: nestedFolderStore, features: features, - permissions: folderPermissions, bus: bus.ProvideBus(tracing.InitializeTracerForTest()), } diff --git a/pkg/services/folder/folderimpl/sqlstore.go b/pkg/services/folder/folderimpl/sqlstore.go index 40797519ee9..4e7767d745d 100644 --- a/pkg/services/folder/folderimpl/sqlstore.go +++ b/pkg/services/folder/folderimpl/sqlstore.go @@ -8,6 +8,7 @@ import ( "github.com/VividCortex/mysqlerr" "github.com/go-sql-driver/mysql" + "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/slugify" diff --git a/pkg/services/grpcserver/health.go b/pkg/services/grpcserver/health.go index c00b3e3cd48..b7ef28f10ae 100644 --- a/pkg/services/grpcserver/health.go +++ b/pkg/services/grpcserver/health.go @@ -3,10 +3,10 @@ package grpcserver import ( "context" - "github.com/grafana/grafana/pkg/setting" - "google.golang.org/grpc/health" "google.golang.org/grpc/health/grpc_health_v1" + + "github.com/grafana/grafana/pkg/setting" ) // HealthService implements GRPC Health Checking Protocol: diff --git a/pkg/services/grpcserver/interceptors/auth.go b/pkg/services/grpcserver/interceptors/auth.go index fc25c51b019..f5f7d25e87a 100644 --- a/pkg/services/grpcserver/interceptors/auth.go +++ b/pkg/services/grpcserver/interceptors/auth.go @@ -4,6 +4,10 @@ import ( "context" "strings" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + apikeygenprefix "github.com/grafana/grafana/pkg/components/apikeygenprefixed" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/accesscontrol" @@ -11,10 +15,6 @@ import ( grpccontext "github.com/grafana/grafana/pkg/services/grpcserver/context" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" - - "google.golang.org/grpc/codes" - "google.golang.org/grpc/metadata" - "google.golang.org/grpc/status" ) type Authenticator interface { diff --git a/pkg/services/grpcserver/interceptors/auth_test.go b/pkg/services/grpcserver/interceptors/auth_test.go index bc1dacd5541..785e3e4ec86 100644 --- a/pkg/services/grpcserver/interceptors/auth_test.go +++ b/pkg/services/grpcserver/interceptors/auth_test.go @@ -4,6 +4,9 @@ import ( "context" "testing" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/metadata" + apikeygenprefix "github.com/grafana/grafana/pkg/components/apikeygenprefixed" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol" @@ -12,8 +15,6 @@ import ( grpccontext "github.com/grafana/grafana/pkg/services/grpcserver/context" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" - "github.com/stretchr/testify/require" - "google.golang.org/grpc/metadata" ) func TestAuthenticator_Authenticate(t *testing.T) { diff --git a/pkg/services/grpcserver/interceptors/tracing.go b/pkg/services/grpcserver/interceptors/tracing.go index ccf805ddad7..9f9fa15f4b5 100644 --- a/pkg/services/grpcserver/interceptors/tracing.go +++ b/pkg/services/grpcserver/interceptors/tracing.go @@ -3,8 +3,9 @@ package interceptors import ( "context" - "github.com/grafana/grafana/pkg/infra/tracing" "google.golang.org/grpc" + + "github.com/grafana/grafana/pkg/infra/tracing" ) const tracingPrefix = "gRPC Server " diff --git a/pkg/services/grpcserver/reflection.go b/pkg/services/grpcserver/reflection.go index 3a4ecd09fe4..2a2751ddeed 100644 --- a/pkg/services/grpcserver/reflection.go +++ b/pkg/services/grpcserver/reflection.go @@ -3,10 +3,10 @@ package grpcserver import ( "context" - "github.com/grafana/grafana/pkg/setting" - "google.golang.org/grpc/reflection" "google.golang.org/grpc/reflection/grpc_reflection_v1alpha" + + "github.com/grafana/grafana/pkg/setting" ) // ReflectionService implements the gRPC Server Reflection Protocol: diff --git a/pkg/services/grpcserver/service.go b/pkg/services/grpcserver/service.go index c7349a48954..bf267fb420e 100644 --- a/pkg/services/grpcserver/service.go +++ b/pkg/services/grpcserver/service.go @@ -6,17 +6,17 @@ import ( "net" "github.com/grafana/grafana-plugin-sdk-go/backend" + grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" + grpcAuth "github.com/grpc-ecosystem/go-grpc-middleware/auth" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/grpcserver/interceptors" "github.com/grafana/grafana/pkg/setting" - grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" - grpcAuth "github.com/grpc-ecosystem/go-grpc-middleware/auth" - - "google.golang.org/grpc" - "google.golang.org/grpc/credentials" ) type Provider interface { diff --git a/pkg/services/guardian/accesscontrol_guardian_test.go b/pkg/services/guardian/accesscontrol_guardian_test.go index b457977a42a..f64b3ddc7cc 100644 --- a/pkg/services/guardian/accesscontrol_guardian_test.go +++ b/pkg/services/guardian/accesscontrol_guardian_test.go @@ -610,10 +610,10 @@ func setupAccessControlGuardianTest(t *testing.T, uid string, permissions []acce require.NoError(t, err) folderPermissions, err := ossaccesscontrol.ProvideFolderPermissions( - setting.NewCfg(), routing.NewRouteRegister(), store, ac, license, &dashboards.FakeDashboardStore{}, ac, teamSvc, userSvc) + setting.NewCfg(), routing.NewRouteRegister(), store, ac, license, &dashboards.FakeDashboardStore{}, foldertest.NewFakeService(), ac, teamSvc, userSvc) require.NoError(t, err) dashboardPermissions, err := ossaccesscontrol.ProvideDashboardPermissions( - setting.NewCfg(), routing.NewRouteRegister(), store, ac, license, &dashboards.FakeDashboardStore{}, ac, teamSvc, userSvc) + setting.NewCfg(), routing.NewRouteRegister(), store, ac, license, &dashboards.FakeDashboardStore{}, foldertest.NewFakeService(), ac, teamSvc, userSvc) require.NoError(t, err) if dashboardSvc == nil { fakeDashboardService := dashboards.NewFakeDashboardService(t) diff --git a/pkg/services/hooks/hooks.go b/pkg/services/hooks/hooks.go index 87e93f7e721..af36933ee20 100644 --- a/pkg/services/hooks/hooks.go +++ b/pkg/services/hooks/hooks.go @@ -2,12 +2,13 @@ package hooks import ( "github.com/grafana/grafana/pkg/api/dtos" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" + "github.com/grafana/grafana/pkg/services/login" ) -type IndexDataHook func(indexData *dtos.IndexViewData, req *models.ReqContext) +type IndexDataHook func(indexData *dtos.IndexViewData, req *contextmodel.ReqContext) -type LoginHook func(loginInfo *models.LoginInfo, req *models.ReqContext) +type LoginHook func(loginInfo *login.LoginInfo, req *contextmodel.ReqContext) type HooksService struct { indexDataHooks []IndexDataHook @@ -22,7 +23,7 @@ func (srv *HooksService) AddIndexDataHook(hook IndexDataHook) { srv.indexDataHooks = append(srv.indexDataHooks, hook) } -func (srv *HooksService) RunIndexDataHooks(indexData *dtos.IndexViewData, req *models.ReqContext) { +func (srv *HooksService) RunIndexDataHooks(indexData *dtos.IndexViewData, req *contextmodel.ReqContext) { for _, hook := range srv.indexDataHooks { hook(indexData, req) } @@ -32,7 +33,7 @@ func (srv *HooksService) AddLoginHook(hook LoginHook) { srv.loginHooks = append(srv.loginHooks, hook) } -func (srv *HooksService) RunLoginHook(loginInfo *models.LoginInfo, req *models.ReqContext) { +func (srv *HooksService) RunLoginHook(loginInfo *login.LoginInfo, req *contextmodel.ReqContext) { for _, hook := range srv.loginHooks { hook(loginInfo, req) } diff --git a/pkg/services/ldap/ldap.go b/pkg/services/ldap/ldap.go index 934a3c0c8e0..8a65c939fcd 100644 --- a/pkg/services/ldap/ldap.go +++ b/pkg/services/ldap/ldap.go @@ -15,7 +15,6 @@ import ( "gopkg.in/ldap.v3" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" ) @@ -33,8 +32,8 @@ type IConnection interface { // IServer is interface for LDAP authorization type IServer interface { - Login(*models.LoginUserQuery) (*models.ExternalUserInfo, error) - Users([]string) ([]*models.ExternalUserInfo, error) + Login(*login.LoginUserQuery) (*login.ExternalUserInfo, error) + Users([]string) ([]*login.ExternalUserInfo, error) Bind() error UserBind(string, string) error Dial() error @@ -202,8 +201,8 @@ func (server *Server) Close() { // // Dial() sets the connection with the server for this Struct. Therefore, we require a // call to Dial() before being able to execute this function. -func (server *Server) Login(query *models.LoginUserQuery) ( - *models.ExternalUserInfo, error, +func (server *Server) Login(query *login.LoginUserQuery) ( + *login.ExternalUserInfo, error, ) { var err error var authAndBind bool @@ -279,7 +278,7 @@ func (server *Server) shouldSingleBind() bool { // Dial() sets the connection with the server for this Struct. Therefore, we require a // call to Dial() before being able to execute this function. func (server *Server) Users(logins []string) ( - []*models.ExternalUserInfo, + []*login.ExternalUserInfo, error, ) { var users [][]*ldap.Entry @@ -293,7 +292,7 @@ func (server *Server) Users(logins []string) ( } if len(users) == 0 { - return []*models.ExternalUserInfo{}, nil + return []*login.ExternalUserInfo{}, nil } serializedUsers, err := server.serializeUsers(users) @@ -361,7 +360,7 @@ func (server *Server) users(logins []string) ( // validateGrafanaUser validates user access. // If there are no ldap group mappings access is true // otherwise a single group must match -func (server *Server) validateGrafanaUser(user *models.ExternalUserInfo) error { +func (server *Server) validateGrafanaUser(user *login.ExternalUserInfo) error { if !SkipOrgRoleSync() && len(server.Config.Groups) > 0 && (len(user.OrgRoles) == 0 && (user.IsGrafanaAdmin == nil || !*user.IsGrafanaAdmin)) { server.log.Error( @@ -423,14 +422,14 @@ func (server *Server) getSearchRequest( } // buildGrafanaUser extracts info from UserInfo model to ExternalUserInfo -func (server *Server) buildGrafanaUser(user *ldap.Entry) (*models.ExternalUserInfo, error) { +func (server *Server) buildGrafanaUser(user *ldap.Entry) (*login.ExternalUserInfo, error) { memberOf, err := server.getMemberOf(user) if err != nil { return nil, err } attrs := server.Config.Attr - extUser := &models.ExternalUserInfo{ + extUser := &login.ExternalUserInfo{ AuthModule: login.LDAPAuthModule, AuthId: user.DN, Name: strings.TrimSpace( @@ -595,8 +594,8 @@ func (server *Server) requestMemberOf(entry *ldap.Entry) ([]string, error) { // from LDAP result to ExternalInfo struct func (server *Server) serializeUsers( entries [][]*ldap.Entry, -) ([]*models.ExternalUserInfo, error) { - var serialized []*models.ExternalUserInfo +) ([]*login.ExternalUserInfo, error) { + var serialized []*login.ExternalUserInfo var users = map[string]struct{}{} for _, dn := range entries { diff --git a/pkg/services/ldap/ldap_groups.go b/pkg/services/ldap/ldap_groups.go index a3e6053e575..be23712313d 100644 --- a/pkg/services/ldap/ldap_groups.go +++ b/pkg/services/ldap/ldap_groups.go @@ -1,9 +1,7 @@ package ldap -import "github.com/grafana/grafana/pkg/models" - type Groups interface { - GetTeams(groups []string, orgIDs []int64) ([]models.TeamOrgGroupDTO, error) + GetTeams(groups []string, orgIDs []int64) ([]TeamOrgGroupDTO, error) } type OSSGroups struct{} @@ -12,6 +10,6 @@ func ProvideGroupsService() *OSSGroups { return &OSSGroups{} } -func (*OSSGroups) GetTeams(_ []string, _ []int64) ([]models.TeamOrgGroupDTO, error) { +func (*OSSGroups) GetTeams(_ []string, _ []int64) ([]TeamOrgGroupDTO, error) { return nil, nil } diff --git a/pkg/services/ldap/ldap_login_test.go b/pkg/services/ldap/ldap_login_test.go index 7b552a8edfa..806812881f9 100644 --- a/pkg/services/ldap/ldap_login_test.go +++ b/pkg/services/ldap/ldap_login_test.go @@ -6,14 +6,13 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "gopkg.in/ldap.v3" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/login" ) -var defaultLogin = &models.LoginUserQuery{ +var defaultLogin = &login.LoginUserQuery{ Username: "user", Password: "pwd", IpAddress: "192.168.1.1:56433", diff --git a/pkg/services/ldap/ldap_private_test.go b/pkg/services/ldap/ldap_private_test.go index b7846f58f6c..1bbcfb6c48c 100644 --- a/pkg/services/ldap/ldap_private_test.go +++ b/pkg/services/ldap/ldap_private_test.go @@ -3,14 +3,12 @@ package ldap import ( "testing" - "github.com/stretchr/testify/require" - "github.com/stretchr/testify/assert" - + "github.com/stretchr/testify/require" "gopkg.in/ldap.v3" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" ) @@ -159,7 +157,7 @@ func TestServer_validateGrafanaUser(t *testing.T) { log: logger.New("test"), } - user := &models.ExternalUserInfo{ + user := &login.ExternalUserInfo{ Login: "markelog", } @@ -179,7 +177,7 @@ func TestServer_validateGrafanaUser(t *testing.T) { log: logger.New("test"), } - user := &models.ExternalUserInfo{ + user := &login.ExternalUserInfo{ Login: "markelog", OrgRoles: map[int64]org.RoleType{ 1: "test", @@ -202,7 +200,7 @@ func TestServer_validateGrafanaUser(t *testing.T) { log: logger.New("test"), } - user := &models.ExternalUserInfo{ + user := &login.ExternalUserInfo{ Login: "markelog", } diff --git a/pkg/services/ldap/model.go b/pkg/services/ldap/model.go new file mode 100644 index 00000000000..302f5bffabe --- /dev/null +++ b/pkg/services/ldap/model.go @@ -0,0 +1,7 @@ +package ldap + +type TeamOrgGroupDTO struct { + TeamName string `json:"teamName"` + OrgName string `json:"orgName"` + GroupDN string `json:"groupDN"` +} diff --git a/pkg/services/libraryelements/api.go b/pkg/services/libraryelements/api.go index 08b46cc7bfc..8443fc4f85a 100644 --- a/pkg/services/libraryelements/api.go +++ b/pkg/services/libraryelements/api.go @@ -7,7 +7,7 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/web" @@ -38,7 +38,7 @@ func (l *LibraryElementService) registerAPIEndpoints() { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (l *LibraryElementService) createHandler(c *models.ReqContext) response.Response { +func (l *LibraryElementService) createHandler(c *contextmodel.ReqContext) response.Response { cmd := CreateLibraryElementCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -88,7 +88,7 @@ func (l *LibraryElementService) createHandler(c *models.ReqContext) response.Res // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (l *LibraryElementService) deleteHandler(c *models.ReqContext) response.Response { +func (l *LibraryElementService) deleteHandler(c *contextmodel.ReqContext) response.Response { id, err := l.deleteLibraryElement(c.Req.Context(), c.SignedInUser, web.Params(c.Req)[":uid"]) if err != nil { return toLibraryElementError(err, "Failed to delete library element") @@ -111,7 +111,7 @@ func (l *LibraryElementService) deleteHandler(c *models.ReqContext) response.Res // 401: unauthorisedError // 404: notFoundError // 500: internalServerError -func (l *LibraryElementService) getHandler(c *models.ReqContext) response.Response { +func (l *LibraryElementService) getHandler(c *contextmodel.ReqContext) response.Response { element, err := l.getLibraryElementByUid(c.Req.Context(), c.SignedInUser, web.Params(c.Req)[":uid"]) if err != nil { return toLibraryElementError(err, "Failed to get library element") @@ -132,7 +132,7 @@ func (l *LibraryElementService) getHandler(c *models.ReqContext) response.Respon // 200: getLibraryElementsResponse // 401: unauthorisedError // 500: internalServerError -func (l *LibraryElementService) getAllHandler(c *models.ReqContext) response.Response { +func (l *LibraryElementService) getAllHandler(c *contextmodel.ReqContext) response.Response { query := searchLibraryElementsQuery{ perPage: c.QueryInt("perPage"), page: c.QueryInt("page"), @@ -166,7 +166,7 @@ func (l *LibraryElementService) getAllHandler(c *models.ReqContext) response.Res // 404: notFoundError // 412: preconditionFailedError // 500: internalServerError -func (l *LibraryElementService) patchHandler(c *models.ReqContext) response.Response { +func (l *LibraryElementService) patchHandler(c *contextmodel.ReqContext) response.Response { cmd := PatchLibraryElementCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -213,7 +213,7 @@ func (l *LibraryElementService) patchHandler(c *models.ReqContext) response.Resp // 401: unauthorisedError // 404: notFoundError // 500: internalServerError -func (l *LibraryElementService) getConnectionsHandler(c *models.ReqContext) response.Response { +func (l *LibraryElementService) getConnectionsHandler(c *contextmodel.ReqContext) response.Response { connections, err := l.getConnections(c.Req.Context(), c.SignedInUser, web.Params(c.Req)[":uid"]) if err != nil { return toLibraryElementError(err, "Failed to get connections") @@ -233,7 +233,7 @@ func (l *LibraryElementService) getConnectionsHandler(c *models.ReqContext) resp // 401: unauthorisedError // 404: notFoundError // 500: internalServerError -func (l *LibraryElementService) getByNameHandler(c *models.ReqContext) response.Response { +func (l *LibraryElementService) getByNameHandler(c *contextmodel.ReqContext) response.Response { elements, err := l.getLibraryElementsByName(c.Req.Context(), c.SignedInUser, web.Params(c.Req)[":name"]) if err != nil { return toLibraryElementError(err, "Failed to get library element") diff --git a/pkg/services/libraryelements/database.go b/pkg/services/libraryelements/database.go index 99f6f1d3057..ae8f3cf8df3 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/infra/db" + "github.com/grafana/grafana/pkg/kinds/librarypanel" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/org" @@ -166,15 +167,15 @@ func (l *LibraryElementService) createLibraryElement(c context.Context, signedIn ConnectedDashboards: 0, Created: element.Created, Updated: element.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: element.CreatedBy, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: element.CreatedBy, Name: signedInUser.Login, - AvatarURL: dtos.GetGravatarUrl(signedInUser.Email), + AvatarUrl: dtos.GetGravatarUrl(signedInUser.Email), }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: element.UpdatedBy, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: element.UpdatedBy, Name: signedInUser.Login, - AvatarURL: dtos.GetGravatarUrl(signedInUser.Email), + AvatarUrl: dtos.GetGravatarUrl(signedInUser.Email), }, }, } @@ -279,15 +280,15 @@ func getLibraryElements(c context.Context, store db.DB, cfg *setting.Cfg, signed ConnectedDashboards: libraryElement.ConnectedDashboards, Created: libraryElement.Created, Updated: libraryElement.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: libraryElement.CreatedBy, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: libraryElement.CreatedBy, Name: libraryElement.CreatedByName, - AvatarURL: dtos.GetGravatarUrl(libraryElement.CreatedByEmail), + AvatarUrl: dtos.GetGravatarUrl(libraryElement.CreatedByEmail), }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: libraryElement.UpdatedBy, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: libraryElement.UpdatedBy, Name: libraryElement.UpdatedByName, - AvatarURL: dtos.GetGravatarUrl(libraryElement.UpdatedByEmail), + AvatarUrl: dtos.GetGravatarUrl(libraryElement.UpdatedByEmail), }, }, } @@ -392,15 +393,15 @@ func (l *LibraryElementService) getAllLibraryElements(c context.Context, signedI ConnectedDashboards: element.ConnectedDashboards, Created: element.Created, Updated: element.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: element.CreatedBy, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: element.CreatedBy, Name: element.CreatedByName, - AvatarURL: dtos.GetGravatarUrl(element.CreatedByEmail), + AvatarUrl: dtos.GetGravatarUrl(element.CreatedByEmail), }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: element.UpdatedBy, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: element.UpdatedBy, Name: element.UpdatedByName, - AvatarURL: dtos.GetGravatarUrl(element.UpdatedByEmail), + AvatarUrl: dtos.GetGravatarUrl(element.UpdatedByEmail), }, }, }) @@ -541,15 +542,15 @@ func (l *LibraryElementService) patchLibraryElement(c context.Context, signedInU ConnectedDashboards: elementInDB.ConnectedDashboards, Created: libraryElement.Created, Updated: libraryElement.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: elementInDB.CreatedBy, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: elementInDB.CreatedBy, Name: elementInDB.CreatedByName, - AvatarURL: dtos.GetGravatarUrl(elementInDB.CreatedByEmail), + AvatarUrl: dtos.GetGravatarUrl(elementInDB.CreatedByEmail), }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: libraryElement.UpdatedBy, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: libraryElement.UpdatedBy, Name: signedInUser.Login, - AvatarURL: dtos.GetGravatarUrl(signedInUser.Email), + AvatarUrl: dtos.GetGravatarUrl(signedInUser.Email), }, }, } @@ -589,10 +590,10 @@ func (l *LibraryElementService) getConnections(c context.Context, signedInUser * ConnectionID: connection.ConnectionID, ConnectionUID: connection.ConnectionUID, Created: connection.Created, - CreatedBy: LibraryElementDTOMetaUser{ - ID: connection.CreatedBy, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: connection.CreatedBy, Name: connection.CreatedByName, - AvatarURL: dtos.GetGravatarUrl(connection.CreatedByEmail), + AvatarUrl: dtos.GetGravatarUrl(connection.CreatedByEmail), }, }) } @@ -638,15 +639,15 @@ func (l *LibraryElementService) getElementsForDashboardID(c context.Context, das ConnectedDashboards: element.ConnectedDashboards, Created: element.Created, Updated: element.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: element.CreatedBy, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: element.CreatedBy, Name: element.CreatedByName, - AvatarURL: dtos.GetGravatarUrl(element.CreatedByEmail), + AvatarUrl: dtos.GetGravatarUrl(element.CreatedByEmail), }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: element.UpdatedBy, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: element.UpdatedBy, Name: element.UpdatedByName, - AvatarURL: dtos.GetGravatarUrl(element.UpdatedByEmail), + AvatarUrl: dtos.GetGravatarUrl(element.UpdatedByEmail), }, }, } diff --git a/pkg/services/libraryelements/libraryelements_create_test.go b/pkg/services/libraryelements/libraryelements_create_test.go index ecc54aca467..ef71130e19b 100644 --- a/pkg/services/libraryelements/libraryelements_create_test.go +++ b/pkg/services/libraryelements/libraryelements_create_test.go @@ -6,6 +6,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/kinds/librarypanel" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/util" ) @@ -45,15 +46,15 @@ func TestCreateLibraryElement(t *testing.T) { ConnectedDashboards: 0, Created: sc.initialResult.Result.Meta.Created, Updated: sc.initialResult.Result.Meta.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: 1, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: "signed_in_user", - AvatarURL: "/avatar/37524e1eb8b3e32850b57db0a19af93b", + AvatarUrl: "/avatar/37524e1eb8b3e32850b57db0a19af93b", }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: 1, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: "signed_in_user", - AvatarURL: "/avatar/37524e1eb8b3e32850b57db0a19af93b", + AvatarUrl: "/avatar/37524e1eb8b3e32850b57db0a19af93b", }, }, }, @@ -94,15 +95,15 @@ func TestCreateLibraryElement(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Meta.Created, Updated: result.Result.Meta.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: 1, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: "signed_in_user", - AvatarURL: "/avatar/37524e1eb8b3e32850b57db0a19af93b", + AvatarUrl: "/avatar/37524e1eb8b3e32850b57db0a19af93b", }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: 1, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: "signed_in_user", - AvatarURL: "/avatar/37524e1eb8b3e32850b57db0a19af93b", + AvatarUrl: "/avatar/37524e1eb8b3e32850b57db0a19af93b", }, }, }, @@ -169,15 +170,15 @@ func TestCreateLibraryElement(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Meta.Created, Updated: result.Result.Meta.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: 1, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: "signed_in_user", - AvatarURL: "/avatar/37524e1eb8b3e32850b57db0a19af93b", + AvatarUrl: "/avatar/37524e1eb8b3e32850b57db0a19af93b", }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: 1, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: "signed_in_user", - AvatarURL: "/avatar/37524e1eb8b3e32850b57db0a19af93b", + AvatarUrl: "/avatar/37524e1eb8b3e32850b57db0a19af93b", }, }, }, diff --git a/pkg/services/libraryelements/libraryelements_get_all_test.go b/pkg/services/libraryelements/libraryelements_get_all_test.go index bac68141c57..2c304eaab2f 100644 --- a/pkg/services/libraryelements/libraryelements_get_all_test.go +++ b/pkg/services/libraryelements/libraryelements_get_all_test.go @@ -8,6 +8,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/kinds/librarypanel" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/search" @@ -81,15 +82,15 @@ func TestGetAllLibraryElements(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Elements[0].Meta.Created, Updated: result.Result.Elements[0].Meta.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: 1, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: 1, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, }, }, @@ -146,15 +147,15 @@ func TestGetAllLibraryElements(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Elements[0].Meta.Created, Updated: result.Result.Elements[0].Meta.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: 1, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: 1, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, }, }, @@ -208,15 +209,15 @@ func TestGetAllLibraryElements(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Elements[0].Meta.Created, Updated: result.Result.Elements[0].Meta.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: 1, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: 1, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, }, }, @@ -243,15 +244,15 @@ func TestGetAllLibraryElements(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Elements[1].Meta.Created, Updated: result.Result.Elements[1].Meta.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: 1, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: 1, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, }, }, @@ -308,15 +309,15 @@ func TestGetAllLibraryElements(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Elements[0].Meta.Created, Updated: result.Result.Elements[0].Meta.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: 1, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: 1, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, }, }, @@ -343,15 +344,15 @@ func TestGetAllLibraryElements(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Elements[1].Meta.Created, Updated: result.Result.Elements[1].Meta.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: 1, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: 1, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, }, }, @@ -429,15 +430,15 @@ func TestGetAllLibraryElements(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Elements[0].Meta.Created, Updated: result.Result.Elements[0].Meta.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: 1, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: 1, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, }, }, @@ -464,15 +465,15 @@ func TestGetAllLibraryElements(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Elements[1].Meta.Created, Updated: result.Result.Elements[1].Meta.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: 1, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: 1, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, }, }, @@ -568,15 +569,15 @@ func TestGetAllLibraryElements(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Elements[0].Meta.Created, Updated: result.Result.Elements[0].Meta.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: 1, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: 1, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, }, }, @@ -665,15 +666,15 @@ func TestGetAllLibraryElements(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Elements[0].Meta.Created, Updated: result.Result.Elements[0].Meta.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: 1, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: 1, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, }, }, @@ -700,15 +701,15 @@ func TestGetAllLibraryElements(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Elements[1].Meta.Created, Updated: result.Result.Elements[1].Meta.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: 1, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: 1, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, }, }, @@ -765,15 +766,15 @@ func TestGetAllLibraryElements(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Elements[0].Meta.Created, Updated: result.Result.Elements[0].Meta.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: 1, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: 1, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, }, }, @@ -830,15 +831,15 @@ func TestGetAllLibraryElements(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Elements[0].Meta.Created, Updated: result.Result.Elements[0].Meta.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: 1, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: 1, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, }, }, @@ -896,15 +897,15 @@ func TestGetAllLibraryElements(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Elements[0].Meta.Created, Updated: result.Result.Elements[0].Meta.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: 1, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: 1, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, }, }, @@ -971,15 +972,15 @@ func TestGetAllLibraryElements(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Elements[0].Meta.Created, Updated: result.Result.Elements[0].Meta.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: 1, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: 1, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, }, }, @@ -1044,15 +1045,15 @@ func TestGetAllLibraryElements(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Elements[0].Meta.Created, Updated: result.Result.Elements[0].Meta.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: 1, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: 1, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, }, }, @@ -1079,15 +1080,15 @@ func TestGetAllLibraryElements(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Elements[1].Meta.Created, Updated: result.Result.Elements[1].Meta.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: 1, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: 1, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, }, }, @@ -1146,15 +1147,15 @@ func TestGetAllLibraryElements(t *testing.T) { ConnectedDashboards: 0, Created: result.Result.Elements[0].Meta.Created, Updated: result.Result.Elements[0].Meta.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: 1, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: 1, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, }, }, diff --git a/pkg/services/libraryelements/libraryelements_get_test.go b/pkg/services/libraryelements/libraryelements_get_test.go index ff587a20d85..ce6d6aecc73 100644 --- a/pkg/services/libraryelements/libraryelements_get_test.go +++ b/pkg/services/libraryelements/libraryelements_get_test.go @@ -5,6 +5,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/kinds/librarypanel" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/web" @@ -54,15 +55,15 @@ func TestGetLibraryElement(t *testing.T) { ConnectedDashboards: 0, Created: res.Result.Meta.Created, Updated: res.Result.Meta.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: 1, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: 1, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, }, }, @@ -149,15 +150,15 @@ func TestGetLibraryElement(t *testing.T) { ConnectedDashboards: 1, Created: res.Result.Meta.Created, Updated: res.Result.Meta.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: 1, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: 1, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, }, }, diff --git a/pkg/services/libraryelements/libraryelements_patch_test.go b/pkg/services/libraryelements/libraryelements_patch_test.go index 51d1f06e822..83e3bd25c22 100644 --- a/pkg/services/libraryelements/libraryelements_patch_test.go +++ b/pkg/services/libraryelements/libraryelements_patch_test.go @@ -3,6 +3,7 @@ package libraryelements import ( "testing" + "github.com/grafana/grafana/pkg/kinds/librarypanel" "github.com/grafana/grafana/pkg/util" "github.com/google/go-cmp/cmp" @@ -68,15 +69,15 @@ func TestPatchLibraryElement(t *testing.T) { ConnectedDashboards: 0, Created: sc.initialResult.Result.Meta.Created, Updated: result.Result.Meta.Updated, - CreatedBy: LibraryElementDTOMetaUser{ - ID: 1, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, - UpdatedBy: LibraryElementDTOMetaUser{ - ID: 1, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: "signed_in_user", - AvatarURL: "/avatar/37524e1eb8b3e32850b57db0a19af93b", + AvatarUrl: "/avatar/37524e1eb8b3e32850b57db0a19af93b", }, }, }, @@ -101,7 +102,7 @@ func TestPatchLibraryElement(t *testing.T) { var result = validateAndUnMarshalResponse(t, resp) sc.initialResult.Result.FolderID = newFolder.ID sc.initialResult.Result.Meta.CreatedBy.Name = userInDbName - sc.initialResult.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar + sc.initialResult.Result.Meta.CreatedBy.AvatarUrl = userInDbAvatar sc.initialResult.Result.Meta.Updated = result.Result.Meta.Updated sc.initialResult.Result.Version = 2 sc.initialResult.Result.Meta.FolderName = "NewFolder" @@ -125,7 +126,7 @@ func TestPatchLibraryElement(t *testing.T) { var result = validateAndUnMarshalResponse(t, resp) sc.initialResult.Result.Name = "New Name" sc.initialResult.Result.Meta.CreatedBy.Name = userInDbName - sc.initialResult.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar + sc.initialResult.Result.Meta.CreatedBy.AvatarUrl = userInDbAvatar sc.initialResult.Result.Meta.Updated = result.Result.Meta.Updated sc.initialResult.Result.Model["title"] = "Text - Library Panel" sc.initialResult.Result.Version = 2 @@ -148,7 +149,7 @@ func TestPatchLibraryElement(t *testing.T) { var result = validateAndUnMarshalResponse(t, resp) sc.initialResult.Result.UID = cmd.UID sc.initialResult.Result.Meta.CreatedBy.Name = userInDbName - sc.initialResult.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar + sc.initialResult.Result.Meta.CreatedBy.AvatarUrl = userInDbAvatar sc.initialResult.Result.Meta.Updated = result.Result.Meta.Updated sc.initialResult.Result.Model["title"] = "Text - Library Panel" sc.initialResult.Result.Version = 2 @@ -225,7 +226,7 @@ func TestPatchLibraryElement(t *testing.T) { "description": "New description", } sc.initialResult.Result.Meta.CreatedBy.Name = userInDbName - sc.initialResult.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar + sc.initialResult.Result.Meta.CreatedBy.AvatarUrl = userInDbAvatar sc.initialResult.Result.Meta.Updated = result.Result.Meta.Updated sc.initialResult.Result.Version = 2 if diff := cmp.Diff(sc.initialResult.Result, result.Result, getCompareOptions()...); diff != "" { @@ -252,7 +253,7 @@ func TestPatchLibraryElement(t *testing.T) { "description": "New description", } sc.initialResult.Result.Meta.CreatedBy.Name = userInDbName - sc.initialResult.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar + sc.initialResult.Result.Meta.CreatedBy.AvatarUrl = userInDbAvatar sc.initialResult.Result.Meta.Updated = result.Result.Meta.Updated sc.initialResult.Result.Version = 2 if diff := cmp.Diff(sc.initialResult.Result, result.Result, getCompareOptions()...); diff != "" { @@ -279,7 +280,7 @@ func TestPatchLibraryElement(t *testing.T) { "description": "A description", } sc.initialResult.Result.Meta.CreatedBy.Name = userInDbName - sc.initialResult.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar + sc.initialResult.Result.Meta.CreatedBy.AvatarUrl = userInDbAvatar sc.initialResult.Result.Meta.Updated = result.Result.Meta.Updated sc.initialResult.Result.Version = 2 if diff := cmp.Diff(sc.initialResult.Result, result.Result, getCompareOptions()...); diff != "" { @@ -295,9 +296,9 @@ func TestPatchLibraryElement(t *testing.T) { sc.ctx.Req.Body = mockRequestBody(cmd) resp := sc.service.patchHandler(sc.reqContext) var result = validateAndUnMarshalResponse(t, resp) - sc.initialResult.Result.Meta.UpdatedBy.ID = int64(2) + sc.initialResult.Result.Meta.UpdatedBy.Id = int64(2) sc.initialResult.Result.Meta.CreatedBy.Name = userInDbName - sc.initialResult.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar + sc.initialResult.Result.Meta.CreatedBy.AvatarUrl = userInDbAvatar sc.initialResult.Result.Meta.Updated = result.Result.Meta.Updated sc.initialResult.Result.Version = 2 if diff := cmp.Diff(sc.initialResult.Result, result.Result, getCompareOptions()...); diff != "" { @@ -393,7 +394,7 @@ func TestPatchLibraryElement(t *testing.T) { "description": "A description", } sc.initialResult.Result.Meta.CreatedBy.Name = userInDbName - sc.initialResult.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar + sc.initialResult.Result.Meta.CreatedBy.AvatarUrl = userInDbAvatar sc.initialResult.Result.Meta.Updated = result.Result.Meta.Updated sc.initialResult.Result.Version = 2 if diff := cmp.Diff(sc.initialResult.Result, result.Result, getCompareOptions()...); diff != "" { diff --git a/pkg/services/libraryelements/libraryelements_permissions_test.go b/pkg/services/libraryelements/libraryelements_permissions_test.go index 3e3c1b4e6dc..b7078f7a5d6 100644 --- a/pkg/services/libraryelements/libraryelements_permissions_test.go +++ b/pkg/services/libraryelements/libraryelements_permissions_test.go @@ -252,9 +252,9 @@ func TestLibraryElementPermissions(t *testing.T) { resp := sc.service.createHandler(sc.reqContext) result := validateAndUnMarshalResponse(t, resp) result.Result.Meta.CreatedBy.Name = userInDbName - result.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar + result.Result.Meta.CreatedBy.AvatarUrl = userInDbAvatar result.Result.Meta.UpdatedBy.Name = userInDbName - result.Result.Meta.UpdatedBy.AvatarURL = userInDbAvatar + result.Result.Meta.UpdatedBy.AvatarUrl = userInDbAvatar result.Result.Meta.FolderName = folder.Title result.Result.Meta.FolderUID = folder.UID results = append(results, result.Result) @@ -275,9 +275,9 @@ func TestLibraryElementPermissions(t *testing.T) { resp := sc.service.createHandler(sc.reqContext) result := validateAndUnMarshalResponse(t, resp) result.Result.Meta.CreatedBy.Name = userInDbName - result.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar + result.Result.Meta.CreatedBy.AvatarUrl = userInDbAvatar result.Result.Meta.UpdatedBy.Name = userInDbName - result.Result.Meta.UpdatedBy.AvatarURL = userInDbAvatar + result.Result.Meta.UpdatedBy.AvatarUrl = userInDbAvatar result.Result.Meta.FolderName = "General" result.Result.Meta.FolderUID = "" sc.reqContext.SignedInUser.OrgRole = testCase.role @@ -315,9 +315,9 @@ func TestLibraryElementPermissions(t *testing.T) { resp := sc.service.createHandler(sc.reqContext) result := validateAndUnMarshalResponse(t, resp) result.Result.Meta.CreatedBy.Name = userInDbName - result.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar + result.Result.Meta.CreatedBy.AvatarUrl = userInDbAvatar result.Result.Meta.UpdatedBy.Name = userInDbName - result.Result.Meta.UpdatedBy.AvatarURL = userInDbAvatar + result.Result.Meta.UpdatedBy.AvatarUrl = userInDbAvatar result.Result.Meta.FolderName = folder.Title result.Result.Meta.FolderUID = folder.UID results = append(results, result.Result) @@ -367,9 +367,9 @@ func TestLibraryElementPermissions(t *testing.T) { resp := sc.service.createHandler(sc.reqContext) result := validateAndUnMarshalResponse(t, resp) result.Result.Meta.CreatedBy.Name = userInDbName - result.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar + result.Result.Meta.CreatedBy.AvatarUrl = userInDbAvatar result.Result.Meta.UpdatedBy.Name = userInDbName - result.Result.Meta.UpdatedBy.AvatarURL = userInDbAvatar + result.Result.Meta.UpdatedBy.AvatarUrl = userInDbAvatar result.Result.Meta.FolderName = "General" sc.reqContext.SignedInUser.OrgRole = testCase.role diff --git a/pkg/services/libraryelements/libraryelements_test.go b/pkg/services/libraryelements/libraryelements_test.go index b9ebdf9f2dc..cce5e99f181 100644 --- a/pkg/services/libraryelements/libraryelements_test.go +++ b/pkg/services/libraryelements/libraryelements_test.go @@ -19,9 +19,11 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/db/dbtest" "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/kinds/librarypanel" "github.com/grafana/grafana/pkg/models" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/alerting" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards/database" dashboardservice "github.com/grafana/grafana/pkg/services/dashboards/service" @@ -164,10 +166,10 @@ func TestGetLibraryPanelConnections(t *testing.T) { ConnectionID: dashInDB.ID, ConnectionUID: dashInDB.UID, Created: res.Result[0].Created, - CreatedBy: LibraryElementDTOMetaUser{ - ID: 1, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, }, }, @@ -258,7 +260,7 @@ func getCreateCommandWithModel(folderID int64, name string, kind models.LibraryE type scenarioContext struct { ctx *web.Context service *LibraryElementService - reqContext *models.ReqContext + reqContext *contextmodel.ReqContext user user.SignedInUser folder *folder.Folder initialResult libraryElementResult @@ -311,12 +313,11 @@ func createFolderWithACL(t *testing.T, sqlStore db.DB, title string, user user.S features := featuremgmt.WithFeatures() cfg.IsFeatureToggleEnabled = features.IsEnabled ac := acmock.New() - folderPermissions := acmock.NewMockedPermissionsService() quotaService := quotatest.New(false, nil) dashboardStore, err := database.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService) require.NoError(t, err) - s := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), cfg, dashboardStore, dashboardStore, nil, features, folderPermissions) + s := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), cfg, dashboardStore, dashboardStore, nil, features) t.Logf("Creating folder with title and UID %q", title) ctx := appcontext.WithUser(context.Background(), &user) folder, err := s.Create(ctx, &folder.CreateFolderCommand{ @@ -445,7 +446,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo service := LibraryElementService{ Cfg: sqlStore.Cfg, SQLStore: sqlStore, - folderService: folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), sqlStore.Cfg, dashboardStore, dashboardStore, nil, features, folderPermissions), + folderService: folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), sqlStore.Cfg, dashboardStore, dashboardStore, nil, features), } // deliberate difference between signed in user and user in db to make it crystal clear @@ -468,7 +469,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo ctx: &webCtx, service: &service, sqlStore: sqlStore, - reqContext: &models.ReqContext{ + reqContext: &contextmodel.ReqContext{ Context: &webCtx, SignedInUser: &usr, }, diff --git a/pkg/services/libraryelements/models.go b/pkg/services/libraryelements/models.go index 54af6dbd9a0..3fd5d1db981 100644 --- a/pkg/services/libraryelements/models.go +++ b/pkg/services/libraryelements/models.go @@ -4,6 +4,8 @@ import ( "encoding/json" "errors" "time" + + "github.com/grafana/grafana/pkg/kinds/librarypanel" ) type LibraryConnectionKind int @@ -93,15 +95,8 @@ type LibraryElementDTOMeta struct { Created time.Time `json:"created"` Updated time.Time `json:"updated"` - CreatedBy LibraryElementDTOMetaUser `json:"createdBy"` - UpdatedBy LibraryElementDTOMetaUser `json:"updatedBy"` -} - -// LibraryElementDTOMetaUser is the meta information for user that creates/changes the library element. -type LibraryElementDTOMetaUser struct { - ID int64 `json:"id"` - Name string `json:"name"` - AvatarURL string `json:"avatarUrl"` + CreatedBy librarypanel.LibraryElementDTOMetaUser `json:"createdBy"` + UpdatedBy librarypanel.LibraryElementDTOMetaUser `json:"updatedBy"` } // libraryElementConnection is the model for library element connections. @@ -129,13 +124,13 @@ type libraryElementConnectionWithMeta struct { // LibraryElementConnectionDTO is the frontend DTO for element connections. type LibraryElementConnectionDTO struct { - ID int64 `json:"id"` - Kind int64 `json:"kind"` - ElementID int64 `json:"elementId"` - ConnectionID int64 `json:"connectionId"` - ConnectionUID string `json:"connectionUid"` - Created time.Time `json:"created"` - CreatedBy LibraryElementDTOMetaUser `json:"createdBy"` + ID int64 `json:"id"` + Kind int64 `json:"kind"` + ElementID int64 `json:"elementId"` + ConnectionID int64 `json:"connectionId"` + ConnectionUID string `json:"connectionUid"` + Created time.Time `json:"created"` + CreatedBy librarypanel.LibraryElementDTOMetaUser `json:"createdBy"` } var ( diff --git a/pkg/services/librarypanels/librarypanels_test.go b/pkg/services/librarypanels/librarypanels_test.go index c0ae1ba624c..c6110ce11f5 100644 --- a/pkg/services/librarypanels/librarypanels_test.go +++ b/pkg/services/librarypanels/librarypanels_test.go @@ -18,6 +18,7 @@ import ( "github.com/grafana/grafana/pkg/infra/db/dbtest" "github.com/grafana/grafana/pkg/infra/slugify" "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/kinds/librarypanel" "github.com/grafana/grafana/pkg/models" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/alerting" @@ -634,15 +635,15 @@ func toLibraryElement(t *testing.T, res libraryelements.LibraryElementDTO) libra ConnectedDashboards: res.Meta.ConnectedDashboards, Created: res.Meta.Created, Updated: res.Meta.Updated, - CreatedBy: libraryelements.LibraryElementDTOMetaUser{ - ID: res.Meta.CreatedBy.ID, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: res.Meta.CreatedBy.Id, Name: res.Meta.CreatedBy.Name, - AvatarURL: res.Meta.CreatedBy.AvatarURL, + AvatarUrl: res.Meta.CreatedBy.AvatarUrl, }, - UpdatedBy: libraryelements.LibraryElementDTOMetaUser{ - ID: res.Meta.UpdatedBy.ID, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: res.Meta.UpdatedBy.Id, Name: res.Meta.UpdatedBy.Name, - AvatarURL: res.Meta.UpdatedBy.AvatarURL, + AvatarUrl: res.Meta.UpdatedBy.AvatarUrl, }, }, } @@ -672,15 +673,15 @@ func getExpected(t *testing.T, res libraryelements.LibraryElementDTO, UID string ConnectedDashboards: 0, Created: res.Meta.Created, Updated: res.Meta.Updated, - CreatedBy: libraryelements.LibraryElementDTOMetaUser{ - ID: 1, + CreatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, - UpdatedBy: libraryelements.LibraryElementDTOMetaUser{ - ID: 1, + UpdatedBy: librarypanel.LibraryElementDTOMetaUser{ + Id: 1, Name: userInDbName, - AvatarURL: userInDbAvatar, + AvatarUrl: userInDbAvatar, }, }, } @@ -724,11 +725,10 @@ func createFolderWithACL(t *testing.T, sqlStore db.DB, title string, user *user. cfg.RBACEnabled = false cfg.IsFeatureToggleEnabled = featuremgmt.WithFeatures().IsEnabled features := featuremgmt.WithFeatures() - folderPermissions := acmock.NewMockedPermissionsService() quotaService := quotatest.New(false, nil) dashboardStore, err := database.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService) require.NoError(t, err) - s := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), cfg, dashboardStore, dashboardStore, nil, features, folderPermissions) + s := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), cfg, dashboardStore, dashboardStore, nil, features) t.Logf("Creating folder with title and UID %q", title) ctx := appcontext.WithUser(context.Background(), user) @@ -836,9 +836,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo features := featuremgmt.WithFeatures() ac := acmock.New() - folderPermissions := acmock.NewMockedPermissionsService() - - folderService := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), cfg, dashboardStore, dashboardStore, nil, features, folderPermissions) + folderService := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), cfg, dashboardStore, dashboardStore, nil, features) elementService := libraryelements.ProvideService(cfg, sqlStore, routing.NewRouteRegister(), folderService) service := LibraryPanelService{ diff --git a/pkg/services/licensing/oss.go b/pkg/services/licensing/oss.go index 34c7fbe15e0..8139d63542d 100644 --- a/pkg/services/licensing/oss.go +++ b/pkg/services/licensing/oss.go @@ -2,7 +2,7 @@ package licensing import ( "github.com/grafana/grafana/pkg/api/dtos" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/hooks" "github.com/grafana/grafana/pkg/services/navtree" "github.com/grafana/grafana/pkg/setting" @@ -54,7 +54,7 @@ func ProvideService(cfg *setting.Cfg, hooksService *hooks.HooksService) *OSSLice Cfg: cfg, HooksService: hooksService, } - l.HooksService.AddIndexDataHook(func(indexData *dtos.IndexViewData, req *models.ReqContext) { + l.HooksService.AddIndexDataHook(func(indexData *dtos.IndexViewData, req *contextmodel.ReqContext) { if !req.IsGrafanaAdmin { return } diff --git a/pkg/services/live/database/tests/storage_test.go b/pkg/services/live/database/tests/storage_test.go index 0f107dcc098..cd3a483e390 100644 --- a/pkg/services/live/database/tests/storage_test.go +++ b/pkg/services/live/database/tests/storage_test.go @@ -4,8 +4,9 @@ import ( "encoding/json" "testing" - "github.com/grafana/grafana/pkg/services/live/model" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/services/live/model" ) func TestIntegrationLiveMessage(t *testing.T) { diff --git a/pkg/services/live/features/broadcast.go b/pkg/services/live/features/broadcast.go index c479a82d505..e85741056e2 100644 --- a/pkg/services/live/features/broadcast.go +++ b/pkg/services/live/features/broadcast.go @@ -3,11 +3,11 @@ package features import ( "context" + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/live/model" "github.com/grafana/grafana/pkg/services/user" - - "github.com/grafana/grafana-plugin-sdk-go/backend" ) var ( diff --git a/pkg/services/live/features/broadcast_mock.go b/pkg/services/live/features/broadcast_mock.go index 19e4afa6b57..54df3472a98 100644 --- a/pkg/services/live/features/broadcast_mock.go +++ b/pkg/services/live/features/broadcast_mock.go @@ -8,6 +8,7 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" + model "github.com/grafana/grafana/pkg/services/live/model" ) diff --git a/pkg/services/live/features/broadcast_test.go b/pkg/services/live/features/broadcast_test.go index 459f07e2b2f..25065087420 100644 --- a/pkg/services/live/features/broadcast_test.go +++ b/pkg/services/live/features/broadcast_test.go @@ -7,9 +7,10 @@ import ( "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" - "github.com/stretchr/testify/require" ) func TestNewBroadcastRunner(t *testing.T) { diff --git a/pkg/services/live/features/comment.go b/pkg/services/live/features/comment.go index b33e882f0cb..49531148fe7 100644 --- a/pkg/services/live/features/comment.go +++ b/pkg/services/live/features/comment.go @@ -4,11 +4,11 @@ import ( "context" "strings" + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/services/comments/commentmodel" "github.com/grafana/grafana/pkg/services/live/model" "github.com/grafana/grafana/pkg/services/user" - - "github.com/grafana/grafana-plugin-sdk-go/backend" ) // CommentHandler manages all the `grafana/comment/*` channels. diff --git a/pkg/services/live/features/plugin.go b/pkg/services/live/features/plugin.go index b97f82a0775..bbb85ec0948 100644 --- a/pkg/services/live/features/plugin.go +++ b/pkg/services/live/features/plugin.go @@ -3,13 +3,13 @@ package features import ( "context" + "github.com/centrifugal/centrifuge" + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/services/live/model" "github.com/grafana/grafana/pkg/services/live/orgchannel" "github.com/grafana/grafana/pkg/services/live/runstream" "github.com/grafana/grafana/pkg/services/user" - - "github.com/centrifugal/centrifuge" - "github.com/grafana/grafana-plugin-sdk-go/backend" ) //go:generate mockgen -destination=plugin_mock.go -package=features github.com/grafana/grafana/pkg/services/live/features PluginContextGetter diff --git a/pkg/services/live/features/plugin_mock.go b/pkg/services/live/features/plugin_mock.go index 5a3f059b540..1056d04c07f 100644 --- a/pkg/services/live/features/plugin_mock.go +++ b/pkg/services/live/features/plugin_mock.go @@ -10,6 +10,7 @@ import ( gomock "github.com/golang/mock/gomock" backend "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/services/user" ) diff --git a/pkg/services/live/live.go b/pkg/services/live/live.go index c763ccff7de..18861520244 100644 --- a/pkg/services/live/live.go +++ b/pkg/services/live/live.go @@ -14,6 +14,14 @@ import ( "sync" "time" + "github.com/centrifugal/centrifuge" + "github.com/go-redis/redis/v8" + "github.com/gobwas/glob" + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/live" + jsoniter "github.com/json-iterator/go" + "golang.org/x/sync/errgroup" + "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" @@ -22,12 +30,12 @@ import ( "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/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/plugincontext" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/comments/commentmodel" + 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" @@ -50,14 +58,6 @@ import ( "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util/errutil" "github.com/grafana/grafana/pkg/web" - - "github.com/centrifugal/centrifuge" - "github.com/go-redis/redis/v8" - "github.com/gobwas/glob" - "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana-plugin-sdk-go/live" - jsoniter "github.com/json-iterator/go" - "golang.org/x/sync/errgroup" ) var ( @@ -350,7 +350,7 @@ func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, r CheckOrigin: checkOrigin, }) - g.websocketHandler = func(ctx *models.ReqContext) { + g.websocketHandler = func(ctx *contextmodel.ReqContext) { user := ctx.SignedInUser // Centrifuge expects Credentials in context with a current user ID. @@ -363,7 +363,7 @@ func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, r wsHandler.ServeHTTP(ctx.Resp, r) } - g.pushWebsocketHandler = func(ctx *models.ReqContext) { + g.pushWebsocketHandler = func(ctx *contextmodel.ReqContext) { user := ctx.SignedInUser newCtx := livecontext.SetContextSignedUser(ctx.Req.Context(), user) newCtx = livecontext.SetContextStreamID(newCtx, web.Params(ctx.Req)[":streamId"]) @@ -371,7 +371,7 @@ func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, r pushWSHandler.ServeHTTP(ctx.Resp, r) } - g.pushPipelineWebsocketHandler = func(ctx *models.ReqContext) { + g.pushPipelineWebsocketHandler = func(ctx *contextmodel.ReqContext) { user := ctx.SignedInUser newCtx := livecontext.SetContextSignedUser(ctx.Req.Context(), user) newCtx = livecontext.SetContextChannelID(newCtx, web.Params(ctx.Req)["*"]) @@ -971,7 +971,7 @@ func (g *GrafanaLive) ClientCount(orgID int64, channel string) (int, error) { return len(p.Presence), nil } -func (g *GrafanaLive) HandleHTTPPublish(ctx *models.ReqContext) response.Response { +func (g *GrafanaLive) HandleHTTPPublish(ctx *contextmodel.ReqContext) response.Response { cmd := dtos.LivePublishCmd{} if err := web.Bind(ctx.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -1047,7 +1047,7 @@ type streamChannelListResponse struct { } // HandleListHTTP returns metadata so the UI can build a nice form -func (g *GrafanaLive) HandleListHTTP(c *models.ReqContext) response.Response { +func (g *GrafanaLive) HandleListHTTP(c *contextmodel.ReqContext) response.Response { var channels []*managedstream.ManagedChannel var err error if g.IsHA() { @@ -1065,7 +1065,7 @@ func (g *GrafanaLive) HandleListHTTP(c *models.ReqContext) response.Response { } // HandleInfoHTTP special http response for -func (g *GrafanaLive) HandleInfoHTTP(ctx *models.ReqContext) response.Response { +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{ @@ -1078,7 +1078,7 @@ func (g *GrafanaLive) HandleInfoHTTP(ctx *models.ReqContext) response.Response { } // HandleChannelRulesListHTTP ... -func (g *GrafanaLive) HandleChannelRulesListHTTP(c *models.ReqContext) response.Response { +func (g *GrafanaLive) HandleChannelRulesListHTTP(c *contextmodel.ReqContext) response.Response { result, err := g.pipelineStorage.ListChannelRules(c.Req.Context(), c.OrgID) if err != nil { return response.Error(http.StatusInternalServerError, "Failed to get channel rules", err) @@ -1139,7 +1139,7 @@ func (s *DryRunRuleStorage) ListChannelRules(_ context.Context, _ int64) ([]pipe } // HandlePipelineConvertTestHTTP ... -func (g *GrafanaLive) HandlePipelineConvertTestHTTP(c *models.ReqContext) response.Response { +func (g *GrafanaLive) HandlePipelineConvertTestHTTP(c *contextmodel.ReqContext) response.Response { body, err := io.ReadAll(c.Req.Body) if err != nil { return response.Error(http.StatusInternalServerError, "Error reading body", err) @@ -1184,7 +1184,7 @@ func (g *GrafanaLive) HandlePipelineConvertTestHTTP(c *models.ReqContext) respon } // HandleChannelRulesPostHTTP ... -func (g *GrafanaLive) HandleChannelRulesPostHTTP(c *models.ReqContext) response.Response { +func (g *GrafanaLive) HandleChannelRulesPostHTTP(c *contextmodel.ReqContext) response.Response { body, err := io.ReadAll(c.Req.Body) if err != nil { return response.Error(http.StatusInternalServerError, "Error reading body", err) @@ -1204,7 +1204,7 @@ func (g *GrafanaLive) HandleChannelRulesPostHTTP(c *models.ReqContext) response. } // HandleChannelRulesPutHTTP ... -func (g *GrafanaLive) HandleChannelRulesPutHTTP(c *models.ReqContext) response.Response { +func (g *GrafanaLive) HandleChannelRulesPutHTTP(c *contextmodel.ReqContext) response.Response { body, err := io.ReadAll(c.Req.Body) if err != nil { return response.Error(http.StatusInternalServerError, "Error reading body", err) @@ -1227,7 +1227,7 @@ func (g *GrafanaLive) HandleChannelRulesPutHTTP(c *models.ReqContext) response.R } // HandleChannelRulesDeleteHTTP ... -func (g *GrafanaLive) HandleChannelRulesDeleteHTTP(c *models.ReqContext) response.Response { +func (g *GrafanaLive) HandleChannelRulesDeleteHTTP(c *contextmodel.ReqContext) response.Response { body, err := io.ReadAll(c.Req.Body) if err != nil { return response.Error(http.StatusInternalServerError, "Error reading body", err) @@ -1248,7 +1248,7 @@ func (g *GrafanaLive) HandleChannelRulesDeleteHTTP(c *models.ReqContext) respons } // HandlePipelineEntitiesListHTTP ... -func (g *GrafanaLive) HandlePipelineEntitiesListHTTP(_ *models.ReqContext) response.Response { +func (g *GrafanaLive) HandlePipelineEntitiesListHTTP(_ *contextmodel.ReqContext) response.Response { return response.JSON(http.StatusOK, util.DynMap{ "subscribers": pipeline.SubscribersRegistry, "dataOutputs": pipeline.DataOutputsRegistry, @@ -1259,7 +1259,7 @@ func (g *GrafanaLive) HandlePipelineEntitiesListHTTP(_ *models.ReqContext) respo } // HandleWriteConfigsListHTTP ... -func (g *GrafanaLive) HandleWriteConfigsListHTTP(c *models.ReqContext) response.Response { +func (g *GrafanaLive) HandleWriteConfigsListHTTP(c *contextmodel.ReqContext) response.Response { backends, err := g.pipelineStorage.ListWriteConfigs(c.Req.Context(), c.OrgID) if err != nil { return response.Error(http.StatusInternalServerError, "Failed to get write configs", err) @@ -1274,7 +1274,7 @@ func (g *GrafanaLive) HandleWriteConfigsListHTTP(c *models.ReqContext) response. } // HandleWriteConfigsPostHTTP ... -func (g *GrafanaLive) HandleWriteConfigsPostHTTP(c *models.ReqContext) response.Response { +func (g *GrafanaLive) HandleWriteConfigsPostHTTP(c *contextmodel.ReqContext) response.Response { body, err := io.ReadAll(c.Req.Body) if err != nil { return response.Error(http.StatusInternalServerError, "Error reading body", err) @@ -1294,7 +1294,7 @@ func (g *GrafanaLive) HandleWriteConfigsPostHTTP(c *models.ReqContext) response. } // HandleWriteConfigsPutHTTP ... -func (g *GrafanaLive) HandleWriteConfigsPutHTTP(c *models.ReqContext) response.Response { +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) @@ -1338,7 +1338,7 @@ func (g *GrafanaLive) HandleWriteConfigsPutHTTP(c *models.ReqContext) response.R } // HandleWriteConfigsDeleteHTTP ... -func (g *GrafanaLive) HandleWriteConfigsDeleteHTTP(c *models.ReqContext) response.Response { +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) diff --git a/pkg/services/live/live_test.go b/pkg/services/live/live_test.go index d4dff995fcd..a448e55d7c0 100644 --- a/pkg/services/live/live_test.go +++ b/pkg/services/live/live_test.go @@ -7,9 +7,9 @@ import ( "testing" "time" - "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/setting" ) func Test_runConcurrentlyIfNeeded_Concurrent(t *testing.T) { diff --git a/pkg/services/live/liveplugin/plugin.go b/pkg/services/live/liveplugin/plugin.go index 72b1f5f8cf8..f35302ef12a 100644 --- a/pkg/services/live/liveplugin/plugin.go +++ b/pkg/services/live/liveplugin/plugin.go @@ -4,14 +4,14 @@ import ( "context" "fmt" + "github.com/centrifugal/centrifuge" + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/plugins/plugincontext" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/live/orgchannel" "github.com/grafana/grafana/pkg/services/live/pipeline" "github.com/grafana/grafana/pkg/services/user" - - "github.com/centrifugal/centrifuge" - "github.com/grafana/grafana-plugin-sdk-go/backend" ) type ChannelLocalPublisher struct { diff --git a/pkg/services/live/managedstream/cache_memory_test.go b/pkg/services/live/managedstream/cache_memory_test.go index ae695fc5e2d..37db71204d9 100644 --- a/pkg/services/live/managedstream/cache_memory_test.go +++ b/pkg/services/live/managedstream/cache_memory_test.go @@ -6,7 +6,6 @@ import ( "testing" "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/stretchr/testify/require" ) diff --git a/pkg/services/live/managedstream/cache_redis.go b/pkg/services/live/managedstream/cache_redis.go index 1fe83211206..b547a11cd20 100644 --- a/pkg/services/live/managedstream/cache_redis.go +++ b/pkg/services/live/managedstream/cache_redis.go @@ -7,10 +7,10 @@ import ( "sync" "time" - "github.com/grafana/grafana/pkg/services/live/orgchannel" - "github.com/go-redis/redis/v8" "github.com/grafana/grafana-plugin-sdk-go/data" + + "github.com/grafana/grafana/pkg/services/live/orgchannel" ) // RedisFrameCache ... diff --git a/pkg/services/live/managedstream/runner.go b/pkg/services/live/managedstream/runner.go index 6ed4de0469d..b9fd21ad5e6 100644 --- a/pkg/services/live/managedstream/runner.go +++ b/pkg/services/live/managedstream/runner.go @@ -8,14 +8,14 @@ import ( "sync" "time" - "github.com/grafana/grafana/pkg/services/live/model" - "github.com/grafana/grafana/pkg/services/live/orgchannel" - "github.com/grafana/grafana/pkg/services/user" - "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana-plugin-sdk-go/live" + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/live/model" + "github.com/grafana/grafana/pkg/services/live/orgchannel" + "github.com/grafana/grafana/pkg/services/user" ) var ( diff --git a/pkg/services/live/pipeline/config.go b/pkg/services/live/pipeline/config.go index f3133b2fc77..13319049f15 100644 --- a/pkg/services/live/pipeline/config.go +++ b/pkg/services/live/pipeline/config.go @@ -2,6 +2,7 @@ package pipeline import ( "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/services/org" ) diff --git a/pkg/services/live/pipeline/data_output_builtin.go b/pkg/services/live/pipeline/data_output_builtin.go index e5175f23ba9..1a4ab62f941 100644 --- a/pkg/services/live/pipeline/data_output_builtin.go +++ b/pkg/services/live/pipeline/data_output_builtin.go @@ -4,10 +4,10 @@ import ( "context" "errors" + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/services/live/livecontext" "github.com/grafana/grafana/pkg/services/live/model" - - "github.com/grafana/grafana-plugin-sdk-go/backend" ) type BuiltinDataOutput struct { diff --git a/pkg/services/live/pipeline/data_output_local_subscribers.go b/pkg/services/live/pipeline/data_output_local_subscribers.go index 186d4b19b1a..5ea8206809e 100644 --- a/pkg/services/live/pipeline/data_output_local_subscribers.go +++ b/pkg/services/live/pipeline/data_output_local_subscribers.go @@ -4,9 +4,9 @@ import ( "context" "fmt" - "github.com/grafana/grafana/pkg/services/live/orgchannel" - "github.com/centrifugal/centrifuge" + + "github.com/grafana/grafana/pkg/services/live/orgchannel" ) type LocalSubscribersDataOutput struct { diff --git a/pkg/services/live/pipeline/devdata.go b/pkg/services/live/pipeline/devdata.go index db7c278bf79..ae17ecf9eba 100644 --- a/pkg/services/live/pipeline/devdata.go +++ b/pkg/services/live/pipeline/devdata.go @@ -10,10 +10,10 @@ import ( "os" "time" - "github.com/grafana/grafana/pkg/services/live/managedstream" - "github.com/centrifugal/centrifuge" "github.com/grafana/grafana-plugin-sdk-go/data" + + "github.com/grafana/grafana/pkg/services/live/managedstream" ) type Data struct { diff --git a/pkg/services/live/pipeline/frame_output_local_subscribers.go b/pkg/services/live/pipeline/frame_output_local_subscribers.go index 1f4179be7e8..396dffd5955 100644 --- a/pkg/services/live/pipeline/frame_output_local_subscribers.go +++ b/pkg/services/live/pipeline/frame_output_local_subscribers.go @@ -5,10 +5,10 @@ import ( "encoding/json" "fmt" - "github.com/grafana/grafana/pkg/services/live/orgchannel" - "github.com/centrifugal/centrifuge" "github.com/grafana/grafana-plugin-sdk-go/data" + + "github.com/grafana/grafana/pkg/services/live/orgchannel" ) type LocalSubscribersFrameOutput struct { diff --git a/pkg/services/live/pipeline/frame_output_managed_stream.go b/pkg/services/live/pipeline/frame_output_managed_stream.go index 705f3735f32..e1388e32baa 100644 --- a/pkg/services/live/pipeline/frame_output_managed_stream.go +++ b/pkg/services/live/pipeline/frame_output_managed_stream.go @@ -3,9 +3,9 @@ package pipeline import ( "context" - "github.com/grafana/grafana/pkg/services/live/managedstream" - "github.com/grafana/grafana-plugin-sdk-go/data" + + "github.com/grafana/grafana/pkg/services/live/managedstream" ) type ManagedStreamFrameOutput struct { diff --git a/pkg/services/live/pipeline/frame_output_remote_write.go b/pkg/services/live/pipeline/frame_output_remote_write.go index 6a75c2e9dbd..02aa4d505d9 100644 --- a/pkg/services/live/pipeline/frame_output_remote_write.go +++ b/pkg/services/live/pipeline/frame_output_remote_write.go @@ -10,8 +10,9 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/grafana/grafana/pkg/services/live/remotewrite" "github.com/prometheus/prometheus/prompb" + + "github.com/grafana/grafana/pkg/services/live/remotewrite" ) const flushInterval = 15 * time.Second diff --git a/pkg/services/live/pipeline/frame_output_remote_write_test.go b/pkg/services/live/pipeline/frame_output_remote_write_test.go index 3ef8acc4172..1780439a5dd 100644 --- a/pkg/services/live/pipeline/frame_output_remote_write_test.go +++ b/pkg/services/live/pipeline/frame_output_remote_write_test.go @@ -4,9 +4,8 @@ import ( "testing" "time" - "github.com/stretchr/testify/require" - "github.com/prometheus/prometheus/prompb" + "github.com/stretchr/testify/require" ) func TestRemoteWriteFrameOutput_sample(t *testing.T) { diff --git a/pkg/services/live/pipeline/frame_storage.go b/pkg/services/live/pipeline/frame_storage.go index 517ebc7bf80..9ac3d297993 100644 --- a/pkg/services/live/pipeline/frame_storage.go +++ b/pkg/services/live/pipeline/frame_storage.go @@ -3,9 +3,9 @@ package pipeline import ( "sync" - "github.com/grafana/grafana/pkg/services/live/orgchannel" - "github.com/grafana/grafana-plugin-sdk-go/data" + + "github.com/grafana/grafana/pkg/services/live/orgchannel" ) // FrameStorage keeps last channel frame in memory. Not usable in HA setup. diff --git a/pkg/services/live/pipeline/pipeline.go b/pkg/services/live/pipeline/pipeline.go index 12e647b62b2..2d4fb8ffcf2 100644 --- a/pkg/services/live/pipeline/pipeline.go +++ b/pkg/services/live/pipeline/pipeline.go @@ -6,9 +6,6 @@ import ( "fmt" "os" - "github.com/grafana/grafana/pkg/services/live/model" - "github.com/grafana/grafana/pkg/services/user" - "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana-plugin-sdk-go/live" @@ -19,6 +16,9 @@ import ( tracesdk "go.opentelemetry.io/otel/sdk/trace" semconv "go.opentelemetry.io/otel/semconv/v1.4.0" "go.opentelemetry.io/otel/trace" + + "github.com/grafana/grafana/pkg/services/live/model" + "github.com/grafana/grafana/pkg/services/user" ) const ( diff --git a/pkg/services/live/pipeline/pipeline_test.go b/pkg/services/live/pipeline/pipeline_test.go index fc3bb61b326..48c94a98a70 100644 --- a/pkg/services/live/pipeline/pipeline_test.go +++ b/pkg/services/live/pipeline/pipeline_test.go @@ -7,7 +7,6 @@ import ( "testing" "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/stretchr/testify/require" ) diff --git a/pkg/services/live/pipeline/rule_builder_storage.go b/pkg/services/live/pipeline/rule_builder_storage.go index a8500e766e8..d800bf7feca 100644 --- a/pkg/services/live/pipeline/rule_builder_storage.go +++ b/pkg/services/live/pipeline/rule_builder_storage.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/centrifugal/centrifuge" + "github.com/grafana/grafana/pkg/services/live/managedstream" "github.com/grafana/grafana/pkg/services/secrets" ) diff --git a/pkg/services/live/pipeline/subscribe_builtin.go b/pkg/services/live/pipeline/subscribe_builtin.go index 4cc82af0601..14e35488ad4 100644 --- a/pkg/services/live/pipeline/subscribe_builtin.go +++ b/pkg/services/live/pipeline/subscribe_builtin.go @@ -3,12 +3,12 @@ package pipeline import ( "context" + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/live" + "github.com/grafana/grafana/pkg/services/live/livecontext" "github.com/grafana/grafana/pkg/services/live/model" "github.com/grafana/grafana/pkg/services/user" - - "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana-plugin-sdk-go/live" ) type BuiltinSubscriber struct { diff --git a/pkg/services/live/pipeline/subscribe_managed_stream.go b/pkg/services/live/pipeline/subscribe_managed_stream.go index 344b732b546..4f668469995 100644 --- a/pkg/services/live/pipeline/subscribe_managed_stream.go +++ b/pkg/services/live/pipeline/subscribe_managed_stream.go @@ -3,11 +3,11 @@ package pipeline import ( "context" + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/services/live/livecontext" "github.com/grafana/grafana/pkg/services/live/managedstream" "github.com/grafana/grafana/pkg/services/live/model" - - "github.com/grafana/grafana-plugin-sdk-go/backend" ) type ManagedStreamSubscriber struct { diff --git a/pkg/services/live/pipeline/subscribe_multiple.go b/pkg/services/live/pipeline/subscribe_multiple.go index a7a6148a99d..9b7b6789236 100644 --- a/pkg/services/live/pipeline/subscribe_multiple.go +++ b/pkg/services/live/pipeline/subscribe_multiple.go @@ -4,6 +4,7 @@ import ( "context" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/services/live/model" ) diff --git a/pkg/services/live/pushhttp/push.go b/pkg/services/live/pushhttp/push.go index 61291cd7e4f..271fe9467d1 100644 --- a/pkg/services/live/pushhttp/push.go +++ b/pkg/services/live/pushhttp/push.go @@ -6,14 +6,14 @@ import ( "io" "net/http" + liveDto "github.com/grafana/grafana-plugin-sdk-go/live" + "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/live" "github.com/grafana/grafana/pkg/services/live/convert" "github.com/grafana/grafana/pkg/services/live/pushurl" "github.com/grafana/grafana/pkg/setting" - - liveDto "github.com/grafana/grafana-plugin-sdk-go/live" "github.com/grafana/grafana/pkg/web" ) @@ -45,7 +45,7 @@ func (g *Gateway) Run(ctx context.Context) error { return ctx.Err() } -func (g *Gateway) Handle(ctx *models.ReqContext) { +func (g *Gateway) Handle(ctx *contextmodel.ReqContext) { streamID := web.Params(ctx.Req)[":streamId"] stream, err := g.GrafanaLive.ManagedStreamRunner.GetOrCreateStream(ctx.SignedInUser.OrgID, liveDto.ScopeStream, streamID) @@ -98,7 +98,7 @@ func (g *Gateway) Handle(ctx *models.ReqContext) { ctx.Resp.WriteHeader(http.StatusOK) } -func (g *Gateway) HandlePipelinePush(ctx *models.ReqContext) { +func (g *Gateway) HandlePipelinePush(ctx *contextmodel.ReqContext) { channelID := web.Params(ctx.Req)["*"] body, err := io.ReadAll(ctx.Req.Body) diff --git a/pkg/services/live/pushws/push_pipeline.go b/pkg/services/live/pushws/push_pipeline.go index b8bba51d810..c96251193c9 100644 --- a/pkg/services/live/pushws/push_pipeline.go +++ b/pkg/services/live/pushws/push_pipeline.go @@ -3,11 +3,11 @@ package pushws import ( "net/http" + "github.com/gorilla/websocket" + "github.com/grafana/grafana/pkg/services/live/convert" "github.com/grafana/grafana/pkg/services/live/livecontext" "github.com/grafana/grafana/pkg/services/live/pipeline" - - "github.com/gorilla/websocket" ) // PipelinePushHandler handles WebSocket client connections that push data to Live Pipeline. diff --git a/pkg/services/live/pushws/push_stream.go b/pkg/services/live/pushws/push_stream.go index d8a66984a18..2a9a42a781e 100644 --- a/pkg/services/live/pushws/push_stream.go +++ b/pkg/services/live/pushws/push_stream.go @@ -3,13 +3,13 @@ package pushws import ( "net/http" + "github.com/gorilla/websocket" + liveDto "github.com/grafana/grafana-plugin-sdk-go/live" + "github.com/grafana/grafana/pkg/services/live/convert" "github.com/grafana/grafana/pkg/services/live/livecontext" "github.com/grafana/grafana/pkg/services/live/managedstream" "github.com/grafana/grafana/pkg/services/live/pushurl" - - "github.com/gorilla/websocket" - liveDto "github.com/grafana/grafana-plugin-sdk-go/live" ) // Handler handles WebSocket client connections that push data to Live. diff --git a/pkg/services/live/runstream/manager.go b/pkg/services/live/runstream/manager.go index f4ab4d7b134..e431ef49623 100644 --- a/pkg/services/live/runstream/manager.go +++ b/pkg/services/live/runstream/manager.go @@ -8,10 +8,10 @@ import ( "sync" "time" + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/user" - - "github.com/grafana/grafana-plugin-sdk-go/backend" ) var ( diff --git a/pkg/services/live/runstream/manager_test.go b/pkg/services/live/runstream/manager_test.go index 0d7be455724..7614f74c195 100644 --- a/pkg/services/live/runstream/manager_test.go +++ b/pkg/services/live/runstream/manager_test.go @@ -8,8 +8,9 @@ import ( "github.com/golang/mock/gomock" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/services/user" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/services/user" ) // wait until channel closed with timeout. diff --git a/pkg/services/live/runstream/mock.go b/pkg/services/live/runstream/mock.go index 9b38e653c90..0ce2e242379 100644 --- a/pkg/services/live/runstream/mock.go +++ b/pkg/services/live/runstream/mock.go @@ -10,6 +10,7 @@ import ( gomock "github.com/golang/mock/gomock" backend "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/services/user" ) diff --git a/pkg/services/live/survey/survey.go b/pkg/services/live/survey/survey.go index 98474f70d73..2ab62fadc76 100644 --- a/pkg/services/live/survey/survey.go +++ b/pkg/services/live/survey/survey.go @@ -10,6 +10,7 @@ import ( "time" "github.com/centrifugal/centrifuge" + "github.com/grafana/grafana/pkg/services/live/managedstream" ) diff --git a/pkg/services/live/telemetry/telegraf/convert.go b/pkg/services/live/telemetry/telegraf/convert.go index b445a444303..fd2616facda 100644 --- a/pkg/services/live/telemetry/telegraf/convert.go +++ b/pkg/services/live/telemetry/telegraf/convert.go @@ -7,9 +7,10 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana-plugin-sdk-go/data/converters" + influx "github.com/influxdata/line-protocol" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/live/telemetry" - influx "github.com/influxdata/line-protocol" ) var ( diff --git a/pkg/services/login/authinfo.go b/pkg/services/login/authinfo.go index 01476e31ef2..ee0146cd5d4 100644 --- a/pkg/services/login/authinfo.go +++ b/pkg/services/login/authinfo.go @@ -3,17 +3,16 @@ package login import ( "context" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/user" ) type AuthInfoService interface { - LookupAndUpdate(ctx context.Context, query *models.GetUserByAuthInfoQuery) (*user.User, error) - GetAuthInfo(ctx context.Context, query *models.GetAuthInfoQuery) error - GetUserLabels(ctx context.Context, query models.GetUserLabelsQuery) (map[int64]string, error) - GetExternalUserInfoByLogin(ctx context.Context, query *models.GetExternalUserInfoByLoginQuery) error - SetAuthInfo(ctx context.Context, cmd *models.SetAuthInfoCommand) error - UpdateAuthInfo(ctx context.Context, cmd *models.UpdateAuthInfoCommand) error + LookupAndUpdate(ctx context.Context, query *GetUserByAuthInfoQuery) (*user.User, error) + GetAuthInfo(ctx context.Context, query *GetAuthInfoQuery) error + GetUserLabels(ctx context.Context, query GetUserLabelsQuery) (map[int64]string, error) + GetExternalUserInfoByLogin(ctx context.Context, query *GetExternalUserInfoByLoginQuery) error + SetAuthInfo(ctx context.Context, cmd *SetAuthInfoCommand) error + UpdateAuthInfo(ctx context.Context, cmd *UpdateAuthInfoCommand) error DeleteUserAuthInfo(ctx context.Context, userID int64) error } @@ -35,6 +34,8 @@ func GetAuthProviderLabel(authModule string) string { return "AzureAD" case "oauth_gitlab": return "GitLab" + case "oauth_okta": + return "Okta" case "oauth_grafana_com", "oauth_grafananet": return "grafana.com" case SAMLAuthModule: diff --git a/pkg/services/login/authinfoservice/database/database.go b/pkg/services/login/authinfoservice/database/database.go index 1aa23b77d17..1d4abbfe85b 100644 --- a/pkg/services/login/authinfoservice/database/database.go +++ b/pkg/services/login/authinfoservice/database/database.go @@ -7,7 +7,6 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/user" @@ -35,19 +34,19 @@ func ProvideAuthInfoStore(sqlStore db.DB, secretsService secrets.Service, userSe return store } -func (s *AuthInfoStore) GetExternalUserInfoByLogin(ctx context.Context, query *models.GetExternalUserInfoByLoginQuery) error { +func (s *AuthInfoStore) GetExternalUserInfoByLogin(ctx context.Context, query *login.GetExternalUserInfoByLoginQuery) error { userQuery := user.GetUserByLoginQuery{LoginOrEmail: query.LoginOrEmail} usr, err := s.userService.GetByLogin(ctx, &userQuery) if err != nil { return err } - authInfoQuery := &models.GetAuthInfoQuery{UserId: usr.ID} + authInfoQuery := &login.GetAuthInfoQuery{UserId: usr.ID} if err := s.GetAuthInfo(ctx, authInfoQuery); err != nil { return err } - query.Result = &models.ExternalUserInfo{ + query.Result = &login.ExternalUserInfo{ UserId: usr.ID, Login: usr.Login, Email: usr.Email, @@ -59,12 +58,12 @@ func (s *AuthInfoStore) GetExternalUserInfoByLogin(ctx context.Context, query *m return nil } -func (s *AuthInfoStore) GetAuthInfo(ctx context.Context, query *models.GetAuthInfoQuery) error { +func (s *AuthInfoStore) GetAuthInfo(ctx context.Context, query *login.GetAuthInfoQuery) error { if query.UserId == 0 && query.AuthId == "" { return user.ErrUserNotFound } - userAuth := &models.UserAuth{ + userAuth := &login.UserAuth{ UserId: query.UserId, AuthModule: query.AuthModule, AuthId: query.AuthId, @@ -110,8 +109,8 @@ func (s *AuthInfoStore) GetAuthInfo(ctx context.Context, query *models.GetAuthIn return nil } -func (s *AuthInfoStore) GetUserLabels(ctx context.Context, query models.GetUserLabelsQuery) (map[int64]string, error) { - userAuths := []models.UserAuth{} +func (s *AuthInfoStore) GetUserLabels(ctx context.Context, query login.GetUserLabelsQuery) (map[int64]string, error) { + userAuths := []login.UserAuth{} params := make([]interface{}, 0, len(query.UserIDs)) for _, id := range query.UserIDs { params = append(params, id) @@ -134,8 +133,8 @@ func (s *AuthInfoStore) GetUserLabels(ctx context.Context, query models.GetUserL return labelMap, nil } -func (s *AuthInfoStore) SetAuthInfo(ctx context.Context, cmd *models.SetAuthInfoCommand) error { - authUser := &models.UserAuth{ +func (s *AuthInfoStore) SetAuthInfo(ctx context.Context, cmd *login.SetAuthInfoCommand) error { + authUser := &login.UserAuth{ UserId: cmd.UserId, AuthModule: cmd.AuthModule, AuthId: cmd.AuthId, @@ -179,10 +178,10 @@ func (s *AuthInfoStore) SetAuthInfo(ctx context.Context, cmd *models.SetAuthInfo // UpdateAuthInfoDate updates the auth info for the user with the latest date. // Avoids overlapping entries hiding the last used one (ex: LDAP->SAML->LDAP). -func (s *AuthInfoStore) UpdateAuthInfoDate(ctx context.Context, authInfo *models.UserAuth) error { +func (s *AuthInfoStore) UpdateAuthInfoDate(ctx context.Context, authInfo *login.UserAuth) error { authInfo.Created = GetTime() - cond := &models.UserAuth{ + cond := &login.UserAuth{ Id: authInfo.Id, UserId: authInfo.UserId, AuthModule: authInfo.AuthModule, @@ -193,8 +192,8 @@ func (s *AuthInfoStore) UpdateAuthInfoDate(ctx context.Context, authInfo *models }) } -func (s *AuthInfoStore) UpdateAuthInfo(ctx context.Context, cmd *models.UpdateAuthInfoCommand) error { - authUser := &models.UserAuth{ +func (s *AuthInfoStore) UpdateAuthInfo(ctx context.Context, cmd *login.UpdateAuthInfoCommand) error { + authUser := &login.UserAuth{ UserId: cmd.UserId, AuthModule: cmd.AuthModule, AuthId: cmd.AuthId, @@ -237,7 +236,7 @@ func (s *AuthInfoStore) UpdateAuthInfo(ctx context.Context, cmd *models.UpdateAu }) } -func (s *AuthInfoStore) DeleteAuthInfo(ctx context.Context, cmd *models.DeleteAuthInfoCommand) error { +func (s *AuthInfoStore) DeleteAuthInfo(ctx context.Context, cmd *login.DeleteAuthInfoCommand) error { return s.sqlStore.WithTransactionalDbSession(ctx, func(sess *db.Session) error { _, err := sess.Delete(cmd.UserAuth) return err diff --git a/pkg/services/login/authinfoservice/service.go b/pkg/services/login/authinfoservice/service.go index 1f248453eb5..e0d6f440994 100644 --- a/pkg/services/login/authinfoservice/service.go +++ b/pkg/services/login/authinfoservice/service.go @@ -6,7 +6,6 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/usagestats" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/user" ) @@ -31,8 +30,8 @@ func ProvideAuthInfoService(userProtectionService login.UserProtectionService, a return s } -func (s *Implementation) LookupAndFix(ctx context.Context, query *models.GetUserByAuthInfoQuery) (bool, *user.User, *models.UserAuth, error) { - authQuery := &models.GetAuthInfoQuery{} +func (s *Implementation) LookupAndFix(ctx context.Context, query *login.GetUserByAuthInfoQuery) (bool, *user.User, *login.UserAuth, error) { + authQuery := &login.GetAuthInfoQuery{} // Try to find the user by auth module and id first if query.AuthModule != "" && query.AuthId != "" { @@ -49,7 +48,7 @@ func (s *Implementation) LookupAndFix(ctx context.Context, query *models.GetUser if query.UserLookupParams.UserID != nil && *query.UserLookupParams.UserID != 0 && *query.UserLookupParams.UserID != authQuery.Result.UserId { - if err := s.authInfoStore.DeleteAuthInfo(ctx, &models.DeleteAuthInfoCommand{ + if err := s.authInfoStore.DeleteAuthInfo(ctx, &login.DeleteAuthInfoCommand{ UserAuth: authQuery.Result, }); err != nil { s.logger.Error("Error removing user_auth entry", "error", err) @@ -61,7 +60,7 @@ func (s *Implementation) LookupAndFix(ctx context.Context, query *models.GetUser if err != nil { if errors.Is(err, user.ErrUserNotFound) { // if the user has been deleted then remove the entry - if errDel := s.authInfoStore.DeleteAuthInfo(ctx, &models.DeleteAuthInfoCommand{ + if errDel := s.authInfoStore.DeleteAuthInfo(ctx, &login.DeleteAuthInfoCommand{ UserAuth: authQuery.Result, }); errDel != nil { s.logger.Error("Error removing user_auth entry", "error", errDel) @@ -81,7 +80,7 @@ func (s *Implementation) LookupAndFix(ctx context.Context, query *models.GetUser return false, nil, nil, user.ErrUserNotFound } -func (s *Implementation) LookupByOneOf(ctx context.Context, params *models.UserLookupParams) (*user.User, error) { +func (s *Implementation) LookupByOneOf(ctx context.Context, params *login.UserLookupParams) (*user.User, error) { var usr *user.User var err error @@ -116,9 +115,9 @@ func (s *Implementation) LookupByOneOf(ctx context.Context, params *models.UserL return usr, nil } -func (s *Implementation) GenericOAuthLookup(ctx context.Context, authModule string, authId string, userID int64) (*models.UserAuth, error) { +func (s *Implementation) GenericOAuthLookup(ctx context.Context, authModule string, authId string, userID int64) (*login.UserAuth, error) { if authModule == genericOAuthModule && userID != 0 { - authQuery := &models.GetAuthInfoQuery{} + authQuery := &login.GetAuthInfoQuery{} authQuery.AuthModule = authModule authQuery.AuthId = authId authQuery.UserId = userID @@ -132,7 +131,7 @@ func (s *Implementation) GenericOAuthLookup(ctx context.Context, authModule stri return nil, nil } -func (s *Implementation) LookupAndUpdate(ctx context.Context, query *models.GetUserByAuthInfoQuery) (*user.User, error) { +func (s *Implementation) LookupAndUpdate(ctx context.Context, query *login.GetUserByAuthInfoQuery) (*user.User, error) { // 1. LookupAndFix = auth info, user, error // TODO: Not a big fan of the fact that we are deleting auth info here, might want to move that foundUser, usr, authInfo, err := s.LookupAndFix(ctx, query) @@ -165,7 +164,7 @@ func (s *Implementation) LookupAndUpdate(ctx context.Context, query *models.GetU if query.AuthModule != "" { if authInfo == nil { - cmd := &models.SetAuthInfoCommand{ + cmd := &login.SetAuthInfoCommand{ UserId: usr.ID, AuthModule: query.AuthModule, AuthId: query.AuthId, @@ -183,26 +182,26 @@ func (s *Implementation) LookupAndUpdate(ctx context.Context, query *models.GetU return usr, nil } -func (s *Implementation) GetAuthInfo(ctx context.Context, query *models.GetAuthInfoQuery) error { +func (s *Implementation) GetAuthInfo(ctx context.Context, query *login.GetAuthInfoQuery) error { return s.authInfoStore.GetAuthInfo(ctx, query) } -func (s *Implementation) GetUserLabels(ctx context.Context, query models.GetUserLabelsQuery) (map[int64]string, error) { +func (s *Implementation) GetUserLabels(ctx context.Context, query login.GetUserLabelsQuery) (map[int64]string, error) { if len(query.UserIDs) == 0 { return map[int64]string{}, nil } return s.authInfoStore.GetUserLabels(ctx, query) } -func (s *Implementation) UpdateAuthInfo(ctx context.Context, cmd *models.UpdateAuthInfoCommand) error { +func (s *Implementation) UpdateAuthInfo(ctx context.Context, cmd *login.UpdateAuthInfoCommand) error { return s.authInfoStore.UpdateAuthInfo(ctx, cmd) } -func (s *Implementation) SetAuthInfo(ctx context.Context, cmd *models.SetAuthInfoCommand) error { +func (s *Implementation) SetAuthInfo(ctx context.Context, cmd *login.SetAuthInfoCommand) error { return s.authInfoStore.SetAuthInfo(ctx, cmd) } -func (s *Implementation) GetExternalUserInfoByLogin(ctx context.Context, query *models.GetExternalUserInfoByLoginQuery) error { +func (s *Implementation) GetExternalUserInfoByLogin(ctx context.Context, query *login.GetExternalUserInfoByLoginQuery) error { return s.authInfoStore.GetExternalUserInfoByLogin(ctx, query) } diff --git a/pkg/services/login/authinfoservice/user_auth_test.go b/pkg/services/login/authinfoservice/user_auth_test.go index f22814020a5..d61e8191482 100644 --- a/pkg/services/login/authinfoservice/user_auth_test.go +++ b/pkg/services/login/authinfoservice/user_auth_test.go @@ -12,7 +12,6 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/usagestats" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/login/authinfoservice/database" "github.com/grafana/grafana/pkg/services/org/orgimpl" @@ -50,22 +49,22 @@ func TestUserAuth(t *testing.T) { t.Run("Can find existing user", func(t *testing.T) { // By Login - login := "loginuser0" + userlogin := "loginuser0" authInfoStore.ExpectedUser = &user.User{ Login: "loginuser0", ID: 1, Email: "user1@test.com", } - query := &models.GetUserByAuthInfoQuery{UserLookupParams: models.UserLookupParams{Login: &login}} + query := &login.GetUserByAuthInfoQuery{UserLookupParams: login.UserLookupParams{Login: &userlogin}} usr, err := srv.LookupAndUpdate(context.Background(), query) require.Nil(t, err) - require.Equal(t, usr.Login, login) + require.Equal(t, usr.Login, userlogin) // By ID id := usr.ID - usr, err = srv.LookupByOneOf(context.Background(), &models.UserLookupParams{ + usr, err = srv.LookupByOneOf(context.Background(), &login.UserLookupParams{ UserID: &id, }) @@ -75,7 +74,7 @@ func TestUserAuth(t *testing.T) { // By Email email := "user1@test.com" - usr, err = srv.LookupByOneOf(context.Background(), &models.UserLookupParams{ + usr, err = srv.LookupByOneOf(context.Background(), &login.UserLookupParams{ Email: &email, }) @@ -86,7 +85,7 @@ func TestUserAuth(t *testing.T) { // Don't find nonexistent user email = "nonexistent@test.com" - usr, err = srv.LookupByOneOf(context.Background(), &models.UserLookupParams{ + usr, err = srv.LookupByOneOf(context.Background(), &login.UserLookupParams{ Email: &email, }) @@ -98,29 +97,29 @@ func TestUserAuth(t *testing.T) { // get nonexistent user_auth entry authInfoStore.ExpectedUser = &user.User{} authInfoStore.ExpectedError = user.ErrUserNotFound - query := &models.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test"} + query := &login.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test"} usr, err := srv.LookupAndUpdate(context.Background(), query) require.Equal(t, user.ErrUserNotFound, err) require.Nil(t, usr) // create user_auth entry - login := "loginuser0" + userlogin := "loginuser0" authInfoStore.ExpectedUser = &user.User{Login: "loginuser0", ID: 1, Email: ""} authInfoStore.ExpectedError = nil - authInfoStore.ExpectedOAuth = &models.UserAuth{Id: 1} - query.UserLookupParams.Login = &login + authInfoStore.ExpectedOAuth = &login.UserAuth{Id: 1} + query.UserLookupParams.Login = &userlogin usr, err = srv.LookupAndUpdate(context.Background(), query) require.Nil(t, err) - require.Equal(t, usr.Login, login) + require.Equal(t, usr.Login, userlogin) // get via user_auth - query = &models.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test"} + query = &login.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test"} usr, err = srv.LookupAndUpdate(context.Background(), query) require.Nil(t, err) - require.Equal(t, usr.Login, login) + require.Equal(t, usr.Login, userlogin) // get with non-matching id idPlusOne := usr.ID + 1 @@ -133,7 +132,7 @@ func TestUserAuth(t *testing.T) { require.Equal(t, usr.Login, "loginuser1") // get via user_auth - query = &models.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test"} + query = &login.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test"} usr, err = srv.LookupAndUpdate(context.Background(), query) require.Nil(t, err) @@ -149,7 +148,7 @@ func TestUserAuth(t *testing.T) { authInfoStore.ExpectedUser = nil authInfoStore.ExpectedError = user.ErrUserNotFound // get via user_auth for deleted user - query = &models.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test"} + query = &login.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test"} usr, err = srv.LookupAndUpdate(context.Background(), query) require.Equal(t, err, user.ErrUserNotFound) @@ -167,10 +166,10 @@ func TestUserAuth(t *testing.T) { token = token.WithExtra(map[string]interface{}{"id_token": idToken}) // Find a user to set tokens on - login := "loginuser0" + userlogin := "loginuser0" authInfoStore.ExpectedUser = &user.User{Login: "loginuser0", ID: 1, Email: ""} authInfoStore.ExpectedError = nil - authInfoStore.ExpectedOAuth = &models.UserAuth{ + authInfoStore.ExpectedOAuth = &login.UserAuth{ Id: 1, OAuthAccessToken: token.AccessToken, OAuthRefreshToken: token.RefreshToken, @@ -179,15 +178,15 @@ func TestUserAuth(t *testing.T) { OAuthExpiry: token.Expiry, } // Calling GetUserByAuthInfoQuery on an existing user will populate an entry in the user_auth table - query := &models.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test", UserLookupParams: models.UserLookupParams{ - Login: &login, + query := &login.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test", UserLookupParams: login.UserLookupParams{ + Login: &userlogin, }} user, err := srv.LookupAndUpdate(context.Background(), query) require.Nil(t, err) - require.Equal(t, user.Login, login) + require.Equal(t, user.Login, userlogin) - cmd := &models.UpdateAuthInfoCommand{ + cmd := &login.UpdateAuthInfoCommand{ UserId: user.ID, AuthId: query.AuthId, AuthModule: query.AuthModule, @@ -197,7 +196,7 @@ func TestUserAuth(t *testing.T) { require.Nil(t, err) - getAuthQuery := &models.GetAuthInfoQuery{ + getAuthQuery := &login.GetAuthInfoQuery{ UserId: user.ID, } @@ -230,34 +229,34 @@ func TestUserAuth(t *testing.T) { } // Find a user to set tokens on - login := "loginuser0" + userlogin := "loginuser0" // Calling srv.LookupAndUpdateQuery on an existing user will populate an entry in the user_auth table // Make the first log-in during the past database.GetTime = func() time.Time { return time.Now().AddDate(0, 0, -2) } - query := &models.GetUserByAuthInfoQuery{AuthModule: "test1", AuthId: "test1", UserLookupParams: models.UserLookupParams{ - Login: &login, + query := &login.GetUserByAuthInfoQuery{AuthModule: "test1", AuthId: "test1", UserLookupParams: login.UserLookupParams{ + Login: &userlogin, }} user, err := srv.LookupAndUpdate(context.Background(), query) database.GetTime = time.Now require.Nil(t, err) - require.Equal(t, user.Login, login) + require.Equal(t, user.Login, userlogin) // Add a second auth module for this user // Have this module's last log-in be more recent database.GetTime = func() time.Time { return time.Now().AddDate(0, 0, -1) } - query = &models.GetUserByAuthInfoQuery{AuthModule: "test2", AuthId: "test2", UserLookupParams: models.UserLookupParams{ - Login: &login, + query = &login.GetUserByAuthInfoQuery{AuthModule: "test2", AuthId: "test2", UserLookupParams: login.UserLookupParams{ + Login: &userlogin, }} user, err = srv.LookupAndUpdate(context.Background(), query) database.GetTime = time.Now require.Nil(t, err) - require.Equal(t, user.Login, login) + require.Equal(t, user.Login, userlogin) authInfoStore.ExpectedOAuth.AuthModule = "test2" // Get the latest entry by not supply an authmodule or authid - getAuthQuery := &models.GetAuthInfoQuery{ + getAuthQuery := &login.GetAuthInfoQuery{ UserId: user.ID, } @@ -267,13 +266,13 @@ func TestUserAuth(t *testing.T) { require.Equal(t, getAuthQuery.Result.AuthModule, "test2") // "log in" again with the first auth module - updateAuthCmd := &models.UpdateAuthInfoCommand{UserId: user.ID, AuthModule: "test1", AuthId: "test1"} + updateAuthCmd := &login.UpdateAuthInfoCommand{UserId: user.ID, AuthModule: "test1", AuthId: "test1"} err = authInfoStore.UpdateAuthInfo(context.Background(), updateAuthCmd) require.Nil(t, err) authInfoStore.ExpectedOAuth.AuthModule = "test1" // Get the latest entry by not supply an authmodule or authid - getAuthQuery = &models.GetAuthInfoQuery{ + getAuthQuery = &login.GetAuthInfoQuery{ UserId: user.ID, } @@ -303,33 +302,33 @@ func TestUserAuth(t *testing.T) { } // Find a user to set tokens on - login := "loginuser0" + userlogin := "loginuser0" fixedTime := time.Now() // Calling srv.LookupAndUpdateQuery on an existing user will populate an entry in the user_auth table // Make the first log-in during the past database.GetTime = func() time.Time { return fixedTime.AddDate(0, 0, -2) } - queryOne := &models.GetUserByAuthInfoQuery{AuthModule: "test1", AuthId: "test1", UserLookupParams: models.UserLookupParams{ - Login: &login, + queryOne := &login.GetUserByAuthInfoQuery{AuthModule: "test1", AuthId: "test1", UserLookupParams: login.UserLookupParams{ + Login: &userlogin, }} user, err := srv.LookupAndUpdate(context.Background(), queryOne) database.GetTime = time.Now require.Nil(t, err) - require.Equal(t, user.Login, login) + require.Equal(t, user.Login, userlogin) // Add a second auth module for this user // Have this module's last log-in be more recent database.GetTime = func() time.Time { return fixedTime.AddDate(0, 0, -1) } - queryTwo := &models.GetUserByAuthInfoQuery{AuthModule: "test2", AuthId: "test2", UserLookupParams: models.UserLookupParams{ - Login: &login, + queryTwo := &login.GetUserByAuthInfoQuery{AuthModule: "test2", AuthId: "test2", UserLookupParams: login.UserLookupParams{ + Login: &userlogin, }} user, err = srv.LookupAndUpdate(context.Background(), queryTwo) require.Nil(t, err) - require.Equal(t, user.Login, login) + require.Equal(t, user.Login, userlogin) // Get the latest entry by not supply an authmodule or authid - getAuthQuery := &models.GetAuthInfoQuery{ + getAuthQuery := &login.GetAuthInfoQuery{ UserId: user.ID, } authInfoStore.ExpectedOAuth.AuthModule = "test2" @@ -343,7 +342,7 @@ func TestUserAuth(t *testing.T) { database.GetTime = func() time.Time { return fixedTime } // add oauth info to auth_info to make sure update date does not overwrite it - updateAuthCmd := &models.UpdateAuthInfoCommand{UserId: user.ID, AuthModule: "test1", AuthId: "test1", OAuthToken: &oauth2.Token{ + updateAuthCmd := &login.UpdateAuthInfoCommand{UserId: user.ID, AuthModule: "test1", AuthId: "test1", OAuthToken: &oauth2.Token{ AccessToken: "access_token", TokenType: "token_type", RefreshToken: "refresh_token", @@ -354,7 +353,7 @@ func TestUserAuth(t *testing.T) { user, err = srv.LookupAndUpdate(context.Background(), queryOne) require.Nil(t, err) - require.Equal(t, user.Login, login) + require.Equal(t, user.Login, userlogin) authInfoStore.ExpectedOAuth.AuthModule = "test1" authInfoStore.ExpectedOAuth.OAuthAccessToken = "access_token" err = authInfoStore.GetAuthInfo(context.Background(), getAuthQuery) @@ -368,7 +367,7 @@ func TestUserAuth(t *testing.T) { database.GetTime = func() time.Time { return fixedTime.AddDate(0, 0, 1) } user, err = srv.LookupAndUpdate(context.Background(), queryTwo) require.Nil(t, err) - require.Equal(t, user.Login, login) + require.Equal(t, user.Login, userlogin) authInfoStore.ExpectedOAuth.AuthModule = "test2" err = authInfoStore.GetAuthInfo(context.Background(), getAuthQuery) @@ -376,7 +375,7 @@ func TestUserAuth(t *testing.T) { require.Equal(t, "test2", getAuthQuery.Result.AuthModule) // Ensure test 1 did not have its entry modified - getAuthQueryUnchanged := &models.GetAuthInfoQuery{ + getAuthQueryUnchanged := &login.GetAuthInfoQuery{ UserId: user.ID, AuthModule: "test1", } @@ -389,23 +388,23 @@ func TestUserAuth(t *testing.T) { t.Run("Can set & locate by generic oauth auth module and user id", func(t *testing.T) { // Find a user to set tokens on - login := "loginuser0" + userlogin := "loginuser0" // Expect to pass since there's a matching login user database.GetTime = func() time.Time { return time.Now().AddDate(0, 0, -2) } - query := &models.GetUserByAuthInfoQuery{AuthModule: genericOAuthModule, AuthId: "", UserLookupParams: models.UserLookupParams{ - Login: &login, + query := &login.GetUserByAuthInfoQuery{AuthModule: genericOAuthModule, AuthId: "", UserLookupParams: login.UserLookupParams{ + Login: &userlogin, }} user, err := srv.LookupAndUpdate(context.Background(), query) database.GetTime = time.Now require.Nil(t, err) - require.Equal(t, user.Login, login) + require.Equal(t, user.Login, userlogin) otherLoginUser := "aloginuser" // Should throw a "user not found" error since there's no matching login user database.GetTime = func() time.Time { return time.Now().AddDate(0, 0, -2) } - query = &models.GetUserByAuthInfoQuery{AuthModule: genericOAuthModule, AuthId: "", UserLookupParams: models.UserLookupParams{ + query = &login.GetUserByAuthInfoQuery{AuthModule: genericOAuthModule, AuthId: "", UserLookupParams: login.UserLookupParams{ Login: &otherLoginUser, }} authInfoStore.ExpectedError = errors.New("some error") @@ -510,7 +509,7 @@ type FakeAuthInfoStore struct { login.AuthInfoService ExpectedError error ExpectedUser *user.User - ExpectedOAuth *models.UserAuth + ExpectedOAuth *login.UserAuth ExpectedDuplicateUserEntries int ExpectedHasDuplicateUserEntries int ExpectedLoginStats login.LoginStats @@ -520,23 +519,23 @@ func newFakeAuthInfoStore() *FakeAuthInfoStore { return &FakeAuthInfoStore{} } -func (f *FakeAuthInfoStore) GetExternalUserInfoByLogin(ctx context.Context, query *models.GetExternalUserInfoByLoginQuery) error { +func (f *FakeAuthInfoStore) GetExternalUserInfoByLogin(ctx context.Context, query *login.GetExternalUserInfoByLoginQuery) error { return f.ExpectedError } -func (f *FakeAuthInfoStore) GetAuthInfo(ctx context.Context, query *models.GetAuthInfoQuery) error { +func (f *FakeAuthInfoStore) GetAuthInfo(ctx context.Context, query *login.GetAuthInfoQuery) error { query.Result = f.ExpectedOAuth return f.ExpectedError } -func (f *FakeAuthInfoStore) SetAuthInfo(ctx context.Context, cmd *models.SetAuthInfoCommand) error { +func (f *FakeAuthInfoStore) SetAuthInfo(ctx context.Context, cmd *login.SetAuthInfoCommand) error { return f.ExpectedError } -func (f *FakeAuthInfoStore) UpdateAuthInfoDate(ctx context.Context, authInfo *models.UserAuth) error { +func (f *FakeAuthInfoStore) UpdateAuthInfoDate(ctx context.Context, authInfo *login.UserAuth) error { return f.ExpectedError } -func (f *FakeAuthInfoStore) UpdateAuthInfo(ctx context.Context, cmd *models.UpdateAuthInfoCommand) error { +func (f *FakeAuthInfoStore) UpdateAuthInfo(ctx context.Context, cmd *login.UpdateAuthInfoCommand) error { return f.ExpectedError } -func (f *FakeAuthInfoStore) DeleteAuthInfo(ctx context.Context, cmd *models.DeleteAuthInfoCommand) error { +func (f *FakeAuthInfoStore) DeleteAuthInfo(ctx context.Context, cmd *login.DeleteAuthInfoCommand) error { return f.ExpectedError } func (f *FakeAuthInfoStore) GetUserById(ctx context.Context, id int64) (*user.User, error) { diff --git a/pkg/services/login/login.go b/pkg/services/login/login.go index bb1ff508e4a..908d1c43038 100644 --- a/pkg/services/login/login.go +++ b/pkg/services/login/login.go @@ -4,7 +4,6 @@ import ( "context" "errors" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/user" ) @@ -15,10 +14,10 @@ var ( ErrSignupNotAllowed = errors.New("system administrator has disabled signup") ) -type TeamSyncFunc func(user *user.User, externalUser *models.ExternalUserInfo) error +type TeamSyncFunc func(user *user.User, externalUser *ExternalUserInfo) error type Service interface { - UpsertUser(ctx context.Context, cmd *models.UpsertUserCommand) error + UpsertUser(ctx context.Context, cmd *UpsertUserCommand) error DisableExternalUser(ctx context.Context, username string) error SetTeamSyncFunc(TeamSyncFunc) } diff --git a/pkg/services/login/loginservice/loginservice.go b/pkg/services/login/loginservice/loginservice.go index 37f38af1373..0b18e713136 100644 --- a/pkg/services/login/loginservice/loginservice.go +++ b/pkg/services/login/loginservice/loginservice.go @@ -5,7 +5,6 @@ import ( "errors" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" @@ -44,10 +43,10 @@ type Implementation struct { } // UpsertUser updates an existing user, or if it doesn't exist, inserts a new one. -func (ls *Implementation) UpsertUser(ctx context.Context, cmd *models.UpsertUserCommand) error { +func (ls *Implementation) UpsertUser(ctx context.Context, cmd *login.UpsertUserCommand) error { extUser := cmd.ExternalUser - usr, errAuthLookup := ls.AuthInfoService.LookupAndUpdate(ctx, &models.GetUserByAuthInfoQuery{ + usr, errAuthLookup := ls.AuthInfoService.LookupAndUpdate(ctx, &login.GetUserByAuthInfoQuery{ AuthModule: extUser.AuthModule, AuthId: extUser.AuthId, UserLookupParams: cmd.UserLookupParams, @@ -109,7 +108,7 @@ func (ls *Implementation) UpsertUser(ctx context.Context, cmd *models.UpsertUser } if extUser.AuthModule != "" { - cmd2 := &models.SetAuthInfoCommand{ + cmd2 := &login.SetAuthInfoCommand{ UserId: cmd.Result.ID, AuthModule: extUser.AuthModule, AuthId: extUser.AuthId, @@ -154,7 +153,8 @@ func (ls *Implementation) UpsertUser(ctx context.Context, cmd *models.UpsertUser } } - if ls.TeamSync != nil { + // There are external providers where we want to completely skip team synchronization see - https://github.com/grafana/grafana/issues/62175 + if ls.TeamSync != nil && !extUser.SkipTeamSync { if errTeamSync := ls.TeamSync(cmd.Result, extUser); errTeamSync != nil { return errTeamSync } @@ -165,7 +165,7 @@ func (ls *Implementation) UpsertUser(ctx context.Context, cmd *models.UpsertUser func (ls *Implementation) DisableExternalUser(ctx context.Context, username string) error { // Check if external user exist in Grafana - userQuery := &models.GetExternalUserInfoByLoginQuery{ + userQuery := &login.GetExternalUserInfoByLoginQuery{ LoginOrEmail: username, } @@ -208,7 +208,7 @@ func (ls *Implementation) SetTeamSyncFunc(teamSyncFunc login.TeamSyncFunc) { ls.TeamSync = teamSyncFunc } -func (ls *Implementation) updateUser(ctx context.Context, usr *user.User, extUser *models.ExternalUserInfo) error { +func (ls *Implementation) updateUser(ctx context.Context, usr *user.User, extUser *login.ExternalUserInfo) error { // sync user info updateCmd := &user.UpdateUserCommand{ UserID: usr.ID, @@ -241,8 +241,8 @@ func (ls *Implementation) updateUser(ctx context.Context, usr *user.User, extUse return ls.userService.Update(ctx, updateCmd) } -func (ls *Implementation) updateUserAuth(ctx context.Context, user *user.User, extUser *models.ExternalUserInfo) error { - updateCmd := &models.UpdateAuthInfoCommand{ +func (ls *Implementation) updateUserAuth(ctx context.Context, user *user.User, extUser *login.ExternalUserInfo) error { + updateCmd := &login.UpdateAuthInfoCommand{ AuthModule: extUser.AuthModule, AuthId: extUser.AuthId, UserId: user.ID, @@ -253,7 +253,7 @@ func (ls *Implementation) updateUserAuth(ctx context.Context, user *user.User, e return ls.AuthInfoService.UpdateAuthInfo(ctx, updateCmd) } -func (ls *Implementation) syncOrgRoles(ctx context.Context, usr *user.User, extUser *models.ExternalUserInfo) error { +func (ls *Implementation) syncOrgRoles(ctx context.Context, usr *user.User, extUser *login.ExternalUserInfo) error { logger.Debug("Syncing organization roles", "id", usr.ID, "extOrgRoles", extUser.OrgRoles) // don't sync org roles if none is specified diff --git a/pkg/services/login/loginservice/loginservice_mock.go b/pkg/services/login/loginservice/loginservice_mock.go index b77ac1b4caa..dbb39e0fcf6 100644 --- a/pkg/services/login/loginservice/loginservice_mock.go +++ b/pkg/services/login/loginservice/loginservice_mock.go @@ -3,7 +3,6 @@ package loginservice import ( "context" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/user" ) @@ -11,11 +10,11 @@ import ( type LoginServiceMock struct { login.Service ExpectedUser *user.User - ExpectedUserFunc func(cmd *models.UpsertUserCommand) *user.User + ExpectedUserFunc func(cmd *login.UpsertUserCommand) *user.User ExpectedError error } -func (s LoginServiceMock) UpsertUser(ctx context.Context, cmd *models.UpsertUserCommand) error { +func (s LoginServiceMock) UpsertUser(ctx context.Context, cmd *login.UpsertUserCommand) error { if s.ExpectedUserFunc != nil { cmd.Result = s.ExpectedUserFunc(cmd) return s.ExpectedError diff --git a/pkg/services/login/loginservice/loginservice_test.go b/pkg/services/login/loginservice/loginservice_test.go index 0798d744045..626c3f519b6 100644 --- a/pkg/services/login/loginservice/loginservice_test.go +++ b/pkg/services/login/loginservice/loginservice_test.go @@ -8,7 +8,9 @@ import ( "github.com/go-kit/log" "github.com/go-kit/log/level" - "github.com/grafana/grafana/pkg/models" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/services/accesscontrol/actest" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/login/logintest" @@ -17,8 +19,6 @@ import ( "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/usertest" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func Test_syncOrgRoles_doesNotBreakWhenTryingToRemoveLastOrgAdmin(t *testing.T) { @@ -65,14 +65,14 @@ func Test_syncOrgRoles_whenTryingToRemoveLastOrgLogsError(t *testing.T) { func Test_teamSync(t *testing.T) { authInfoMock := &logintest.AuthInfoServiceFake{} - login := Implementation{ + loginsvc := Implementation{ QuotaService: quotatest.New(false, nil), AuthInfoService: authInfoMock, } email := "test_user@example.org" - upserCmd := &models.UpsertUserCommand{ExternalUser: &models.ExternalUserInfo{Email: email}, - UserLookupParams: models.UserLookupParams{Email: &email}} + upsertCmd := &login.UpsertUserCommand{ExternalUser: &login.ExternalUserInfo{Email: email}, + UserLookupParams: login.UserLookupParams{Email: &email}} expectedUser := &user.User{ ID: 1, Email: email, @@ -82,33 +82,56 @@ func Test_teamSync(t *testing.T) { authInfoMock.ExpectedUser = expectedUser var actualUser *user.User - var actualExternalUser *models.ExternalUserInfo + var actualExternalUser *login.ExternalUserInfo - t.Run("login.TeamSync should not be called when nil", func(t *testing.T) { - err := login.UpsertUser(context.Background(), upserCmd) + t.Run("login.TeamSync should not be called when nil", func(t *testing.T) { + err := loginsvc.UpsertUser(context.Background(), upsertCmd) require.Nil(t, err) assert.Nil(t, actualUser) assert.Nil(t, actualExternalUser) t.Run("login.TeamSync should be called when not nil", func(t *testing.T) { - teamSyncFunc := func(user *user.User, externalUser *models.ExternalUserInfo) error { + teamSyncFunc := func(user *user.User, externalUser *login.ExternalUserInfo) error { actualUser = user actualExternalUser = externalUser return nil } - login.TeamSync = teamSyncFunc - err := login.UpsertUser(context.Background(), upserCmd) + loginsvc.TeamSync = teamSyncFunc + err := loginsvc.UpsertUser(context.Background(), upsertCmd) require.Nil(t, err) assert.Equal(t, actualUser, expectedUser) - assert.Equal(t, actualExternalUser, upserCmd.ExternalUser) + assert.Equal(t, actualExternalUser, upsertCmd.ExternalUser) + }) + + t.Run("login.TeamSync should not be called when not nil and skipTeamSync is set for externalUserInfo", func(t *testing.T) { + var actualUser *user.User + var actualExternalUser *login.ExternalUserInfo + upsertCmdSkipTeamSync := &login.UpsertUserCommand{ + ExternalUser: &login.ExternalUserInfo{ + Email: email, + // sending in ExternalUserInfo with SkipTeamSync yields no team sync + SkipTeamSync: true, + }, + UserLookupParams: login.UserLookupParams{Email: &email}, + } + teamSyncFunc := func(user *user.User, externalUser *login.ExternalUserInfo) error { + actualUser = user + actualExternalUser = externalUser + return nil + } + loginsvc.TeamSync = teamSyncFunc + err := loginsvc.UpsertUser(context.Background(), upsertCmdSkipTeamSync) + require.Nil(t, err) + assert.Nil(t, actualUser) + assert.Nil(t, actualExternalUser) }) t.Run("login.TeamSync should propagate its errors to the caller", func(t *testing.T) { - teamSyncFunc := func(user *user.User, externalUser *models.ExternalUserInfo) error { + teamSyncFunc := func(user *user.User, externalUser *login.ExternalUserInfo) error { return errors.New("teamsync test error") } - login.TeamSync = teamSyncFunc - err := login.UpsertUser(context.Background(), upserCmd) + loginsvc.TeamSync = teamSyncFunc + err := loginsvc.UpsertUser(context.Background(), upsertCmd) require.Error(t, err) }) }) @@ -143,8 +166,8 @@ func createUserOrgDTO() []*org.UserOrgDTO { return users } -func createSimpleExternalUser() models.ExternalUserInfo { - externalUser := models.ExternalUserInfo{ +func createSimpleExternalUser() login.ExternalUserInfo { + externalUser := login.ExternalUserInfo{ AuthModule: login.LDAPAuthModule, OrgRoles: map[int64]org.RoleType{ 1: org.RoleViewer, diff --git a/pkg/services/login/logintest/logintest.go b/pkg/services/login/logintest/logintest.go index 1ca3d532ca9..0c9c2b82944 100644 --- a/pkg/services/login/logintest/logintest.go +++ b/pkg/services/login/logintest/logintest.go @@ -3,14 +3,13 @@ package logintest import ( "context" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/user" ) type LoginServiceFake struct{} -func (l *LoginServiceFake) UpsertUser(ctx context.Context, cmd *models.UpsertUserCommand) error { +func (l *LoginServiceFake) UpsertUser(ctx context.Context, cmd *login.UpsertUserCommand) error { return nil } func (l *LoginServiceFake) DisableExternalUser(ctx context.Context, username string) error { @@ -21,17 +20,17 @@ func (l *LoginServiceFake) SetTeamSyncFunc(login.TeamSyncFunc) {} type AuthInfoServiceFake struct { login.AuthInfoService LatestUserID int64 - ExpectedUserAuth *models.UserAuth + ExpectedUserAuth *login.UserAuth ExpectedUser *user.User - ExpectedExternalUser *models.ExternalUserInfo + ExpectedExternalUser *login.ExternalUserInfo ExpectedError error ExpectedLabels map[int64]string - SetAuthInfoFn func(ctx context.Context, cmd *models.SetAuthInfoCommand) error - UpdateAuthInfoFn func(ctx context.Context, cmd *models.UpdateAuthInfoCommand) error + SetAuthInfoFn func(ctx context.Context, cmd *login.SetAuthInfoCommand) error + UpdateAuthInfoFn func(ctx context.Context, cmd *login.UpdateAuthInfoCommand) error } -func (a *AuthInfoServiceFake) LookupAndUpdate(ctx context.Context, query *models.GetUserByAuthInfoQuery) (*user.User, error) { +func (a *AuthInfoServiceFake) LookupAndUpdate(ctx context.Context, query *login.GetUserByAuthInfoQuery) (*user.User, error) { if query.UserLookupParams.UserID != nil { a.LatestUserID = *query.UserLookupParams.UserID } else { @@ -40,17 +39,17 @@ func (a *AuthInfoServiceFake) LookupAndUpdate(ctx context.Context, query *models return a.ExpectedUser, a.ExpectedError } -func (a *AuthInfoServiceFake) GetAuthInfo(ctx context.Context, query *models.GetAuthInfoQuery) error { +func (a *AuthInfoServiceFake) GetAuthInfo(ctx context.Context, query *login.GetAuthInfoQuery) error { a.LatestUserID = query.UserId query.Result = a.ExpectedUserAuth return a.ExpectedError } -func (a *AuthInfoServiceFake) GetUserLabels(ctx context.Context, query models.GetUserLabelsQuery) (map[int64]string, error) { +func (a *AuthInfoServiceFake) GetUserLabels(ctx context.Context, query login.GetUserLabelsQuery) (map[int64]string, error) { return a.ExpectedLabels, a.ExpectedError } -func (a *AuthInfoServiceFake) SetAuthInfo(ctx context.Context, cmd *models.SetAuthInfoCommand) error { +func (a *AuthInfoServiceFake) SetAuthInfo(ctx context.Context, cmd *login.SetAuthInfoCommand) error { if a.SetAuthInfoFn != nil { return a.SetAuthInfoFn(ctx, cmd) } @@ -58,7 +57,7 @@ func (a *AuthInfoServiceFake) SetAuthInfo(ctx context.Context, cmd *models.SetAu return a.ExpectedError } -func (a *AuthInfoServiceFake) UpdateAuthInfo(ctx context.Context, cmd *models.UpdateAuthInfoCommand) error { +func (a *AuthInfoServiceFake) UpdateAuthInfo(ctx context.Context, cmd *login.UpdateAuthInfoCommand) error { if a.UpdateAuthInfoFn != nil { return a.UpdateAuthInfoFn(ctx, cmd) } @@ -66,7 +65,7 @@ func (a *AuthInfoServiceFake) UpdateAuthInfo(ctx context.Context, cmd *models.Up return a.ExpectedError } -func (a *AuthInfoServiceFake) GetExternalUserInfoByLogin(ctx context.Context, query *models.GetExternalUserInfoByLoginQuery) error { +func (a *AuthInfoServiceFake) GetExternalUserInfoByLogin(ctx context.Context, query *login.GetExternalUserInfoByLoginQuery) error { query.Result = a.ExpectedExternalUser return a.ExpectedError } @@ -80,7 +79,7 @@ type AuthenticatorFake struct { ExpectedError error } -func (a *AuthenticatorFake) AuthenticateUser(c context.Context, query *models.LoginUserQuery) error { +func (a *AuthenticatorFake) AuthenticateUser(c context.Context, query *login.LoginUserQuery) error { query.User = a.ExpectedUser return a.ExpectedError } diff --git a/pkg/services/login/model.go b/pkg/services/login/model.go index a69b7d9fcba..60a3526872b 100644 --- a/pkg/services/login/model.go +++ b/pkg/services/login/model.go @@ -1,10 +1,17 @@ package login import ( + "fmt" "sync" "time" "github.com/prometheus/client_golang/prometheus" + "golang.org/x/oauth2" + + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" + "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/setting" ) type LoginStats struct { @@ -30,3 +37,122 @@ var ( Once sync.Once Initialised bool = false ) + +type UserAuth struct { + Id int64 + UserId int64 + AuthModule string + AuthId string + Created time.Time + OAuthAccessToken string + OAuthRefreshToken string + OAuthIdToken string + OAuthTokenType string + OAuthExpiry time.Time +} + +type ExternalUserInfo struct { + OAuthToken *oauth2.Token + AuthModule string + AuthId string + UserId int64 + Email string + Login string + Name string + Groups []string + OrgRoles map[int64]org.RoleType + IsGrafanaAdmin *bool // This is a pointer to know if we should sync this or not (nil = ignore sync) + IsDisabled bool + SkipTeamSync bool +} + +func (e *ExternalUserInfo) String() string { + return fmt.Sprintf("%+v", *e) +} + +type LoginInfo struct { + AuthModule string + User *user.User + ExternalUser ExternalUserInfo + LoginUsername string + HTTPStatus int + Error error +} + +// RequestURIKey is used as key to save request URI in contexts +// (used for the Enterprise auditing feature) +type RequestURIKey struct{} + +// --------------------- +// COMMANDS + +type UpsertUserCommand struct { + ReqContext *contextmodel.ReqContext + ExternalUser *ExternalUserInfo + UserLookupParams + SignupAllowed bool + + Result *user.User +} + +type SetAuthInfoCommand struct { + AuthModule string + AuthId string + UserId int64 + OAuthToken *oauth2.Token +} + +type UpdateAuthInfoCommand struct { + AuthModule string + AuthId string + UserId int64 + OAuthToken *oauth2.Token +} + +type DeleteAuthInfoCommand struct { + UserAuth *UserAuth +} + +// ---------------------- +// QUERIES + +type LoginUserQuery struct { + ReqContext *contextmodel.ReqContext + Username string + Password string + User *user.User + IpAddress string + AuthModule string + Cfg *setting.Cfg +} + +type GetUserByAuthInfoQuery struct { + AuthModule string + AuthId string + UserLookupParams +} + +type UserLookupParams struct { + // Describes lookup order as well + UserID *int64 // if set, will try to find the user by id + Email *string // if set, will try to find the user by email + Login *string // if set, will try to find the user by login +} + +type GetExternalUserInfoByLoginQuery struct { + LoginOrEmail string + + Result *ExternalUserInfo +} + +type GetAuthInfoQuery struct { + UserId int64 + AuthModule string + AuthId string + + Result *UserAuth +} + +type GetUserLabelsQuery struct { + UserIDs []int64 +} diff --git a/pkg/services/login/userprotection.go b/pkg/services/login/userprotection.go index f77b6401244..4f94d6a030b 100644 --- a/pkg/services/login/userprotection.go +++ b/pkg/services/login/userprotection.go @@ -3,7 +3,6 @@ package login import ( "context" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/user" ) @@ -12,13 +11,13 @@ type UserProtectionService interface { } type Store interface { - GetExternalUserInfoByLogin(ctx context.Context, query *models.GetExternalUserInfoByLoginQuery) error - GetAuthInfo(ctx context.Context, query *models.GetAuthInfoQuery) error - GetUserLabels(ctx context.Context, query models.GetUserLabelsQuery) (map[int64]string, error) - SetAuthInfo(ctx context.Context, cmd *models.SetAuthInfoCommand) error - UpdateAuthInfo(ctx context.Context, cmd *models.UpdateAuthInfoCommand) error - UpdateAuthInfoDate(ctx context.Context, authInfo *models.UserAuth) error - DeleteAuthInfo(ctx context.Context, cmd *models.DeleteAuthInfoCommand) error + GetExternalUserInfoByLogin(ctx context.Context, query *GetExternalUserInfoByLoginQuery) error + GetAuthInfo(ctx context.Context, query *GetAuthInfoQuery) error + GetUserLabels(ctx context.Context, query GetUserLabelsQuery) (map[int64]string, error) + SetAuthInfo(ctx context.Context, cmd *SetAuthInfoCommand) error + UpdateAuthInfo(ctx context.Context, cmd *UpdateAuthInfoCommand) error + UpdateAuthInfoDate(ctx context.Context, authInfo *UserAuth) error + DeleteAuthInfo(ctx context.Context, cmd *DeleteAuthInfoCommand) error GetUserById(ctx context.Context, id int64) (*user.User, error) GetUserByLogin(ctx context.Context, login string) (*user.User, error) GetUserByEmail(ctx context.Context, email string) (*user.User, error) diff --git a/pkg/services/loginattempt/loginattemptimpl/login_attempt_test.go b/pkg/services/loginattempt/loginattemptimpl/login_attempt_test.go index 84e93e0049f..e6ba7c31fe4 100644 --- a/pkg/services/loginattempt/loginattemptimpl/login_attempt_test.go +++ b/pkg/services/loginattempt/loginattemptimpl/login_attempt_test.go @@ -4,8 +4,9 @@ import ( "context" "testing" - "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/assert" + + "github.com/grafana/grafana/pkg/setting" ) func TestService_Validate(t *testing.T) { diff --git a/pkg/services/multildap/multidap_mock.go b/pkg/services/multildap/multidap_mock.go index d59ca876f00..31b2557d0a3 100644 --- a/pkg/services/multildap/multidap_mock.go +++ b/pkg/services/multildap/multidap_mock.go @@ -1,8 +1,8 @@ package multildap import ( - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/ldap" + "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/user" ) @@ -16,25 +16,25 @@ type MultiLDAPmock struct { ExpectedErr error } -func (m *MultiLDAPmock) Login(query *models.LoginUserQuery) ( - *models.ExternalUserInfo, error, +func (m *MultiLDAPmock) Login(query *login.LoginUserQuery) ( + *login.ExternalUserInfo, error, ) { m.LoginCalled = true query.User = m.UserInfo query.AuthModule = m.AuthModule - result := &models.ExternalUserInfo{ + result := &login.ExternalUserInfo{ UserId: m.ID, } return result, m.ExpectedErr } -func (m *MultiLDAPmock) User(login string) ( - *models.ExternalUserInfo, +func (m *MultiLDAPmock) User(loginstr string) ( + *login.ExternalUserInfo, ldap.ServerConfig, error, ) { m.UserCalled = true - result := &models.ExternalUserInfo{ + result := &login.ExternalUserInfo{ UserId: m.ID, } return result, ldap.ServerConfig{}, nil diff --git a/pkg/services/multildap/multildap.go b/pkg/services/multildap/multildap.go index b9ea8bd9ef3..25ec86fde72 100644 --- a/pkg/services/multildap/multildap.go +++ b/pkg/services/multildap/multildap.go @@ -4,8 +4,8 @@ import ( "errors" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/ldap" + "github.com/grafana/grafana/pkg/services/login" ) // logger to log @@ -43,16 +43,16 @@ type ServerStatus struct { // IMultiLDAP is interface for MultiLDAP type IMultiLDAP interface { Ping() ([]*ServerStatus, error) - Login(query *models.LoginUserQuery) ( - *models.ExternalUserInfo, error, + Login(query *login.LoginUserQuery) ( + *login.ExternalUserInfo, error, ) Users(logins []string) ( - []*models.ExternalUserInfo, error, + []*login.ExternalUserInfo, error, ) User(login string) ( - *models.ExternalUserInfo, ldap.ServerConfig, error, + *login.ExternalUserInfo, ldap.ServerConfig, error, ) } @@ -99,8 +99,8 @@ func (multiples *MultiLDAP) Ping() ([]*ServerStatus, error) { } // Login tries to log in the user in multiples LDAP -func (multiples *MultiLDAP) Login(query *models.LoginUserQuery) ( - *models.ExternalUserInfo, error, +func (multiples *MultiLDAP) Login(query *login.LoginUserQuery) ( + *login.ExternalUserInfo, error, ) { if len(multiples.configs) == 0 { return nil, ErrNoLDAPServers @@ -157,7 +157,7 @@ func (multiples *MultiLDAP) Login(query *models.LoginUserQuery) ( // User attempts to find an user by login/username by searching into all of the configured LDAP servers. Then, if the user is found it returns the user alongisde the server it was found. func (multiples *MultiLDAP) User(login string) ( - *models.ExternalUserInfo, + *login.ExternalUserInfo, ldap.ServerConfig, error, ) { @@ -200,10 +200,10 @@ func (multiples *MultiLDAP) User(login string) ( // Users gets users from multiple LDAP servers func (multiples *MultiLDAP) Users(logins []string) ( - []*models.ExternalUserInfo, + []*login.ExternalUserInfo, error, ) { - var result []*models.ExternalUserInfo + var result []*login.ExternalUserInfo if len(multiples.configs) == 0 { return nil, ErrNoLDAPServers diff --git a/pkg/services/multildap/multildap_test.go b/pkg/services/multildap/multildap_test.go index cbf33532b2d..e15638b4ca8 100644 --- a/pkg/services/multildap/multildap_test.go +++ b/pkg/services/multildap/multildap_test.go @@ -4,11 +4,11 @@ import ( "errors" "testing" - "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/ldap" - "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/services/ldap" + "github.com/grafana/grafana/pkg/services/login" + //TODO(sh0rez): remove once import cycle resolved _ "github.com/grafana/grafana/pkg/api/response" ) @@ -71,7 +71,7 @@ func TestMultiLDAP(t *testing.T) { setup() multi := New([]*ldap.ServerConfig{}) - _, err := multi.Login(&models.LoginUserQuery{}) + _, err := multi.Login(&login.LoginUserQuery{}) require.Error(t, err) require.Equal(t, ErrNoLDAPServers, err) @@ -89,7 +89,7 @@ func TestMultiLDAP(t *testing.T) { {}, {}, }) - _, err := multi.Login(&models.LoginUserQuery{}) + _, err := multi.Login(&login.LoginUserQuery{}) require.Error(t, err) require.Equal(t, expected, err) @@ -104,7 +104,7 @@ func TestMultiLDAP(t *testing.T) { multi := New([]*ldap.ServerConfig{ {}, {}, }) - _, err := multi.Login(&models.LoginUserQuery{}) + _, err := multi.Login(&login.LoginUserQuery{}) require.Equal(t, 2, mock.dialCalledTimes) require.Equal(t, 2, mock.loginCalledTimes) @@ -118,14 +118,14 @@ func TestMultiLDAP(t *testing.T) { t.Run("Should get login result", func(t *testing.T) { mock := setup() - mock.loginReturn = &models.ExternalUserInfo{ + mock.loginReturn = &login.ExternalUserInfo{ Login: "killa", } multi := New([]*ldap.ServerConfig{ {}, {}, }) - result, err := multi.Login(&models.LoginUserQuery{}) + result, err := multi.Login(&login.LoginUserQuery{}) require.Equal(t, 1, mock.dialCalledTimes) require.Equal(t, 1, mock.loginCalledTimes) @@ -145,7 +145,7 @@ func TestMultiLDAP(t *testing.T) { multi := New([]*ldap.ServerConfig{ {}, {}, }) - _, err := multi.Login(&models.LoginUserQuery{}) + _, err := multi.Login(&login.LoginUserQuery{}) require.Equal(t, 2, mock.dialCalledTimes) require.Equal(t, 2, mock.loginCalledTimes) @@ -164,7 +164,7 @@ func TestMultiLDAP(t *testing.T) { multi := New([]*ldap.ServerConfig{ {}, {}, }) - _, err := multi.Login(&models.LoginUserQuery{}) + _, err := multi.Login(&login.LoginUserQuery{}) require.Equal(t, 2, mock.dialCalledTimes) require.Equal(t, 2, mock.loginCalledTimes) @@ -184,7 +184,7 @@ func TestMultiLDAP(t *testing.T) { multi := New([]*ldap.ServerConfig{ {}, {}, }) - _, err := multi.Login(&models.LoginUserQuery{}) + _, err := multi.Login(&login.LoginUserQuery{}) require.Equal(t, 2, mock.dialCalledTimes) @@ -202,7 +202,7 @@ func TestMultiLDAP(t *testing.T) { multi := New([]*ldap.ServerConfig{ {}, {}, }) - _, err := multi.Login(&models.LoginUserQuery{}) + _, err := multi.Login(&login.LoginUserQuery{}) require.Equal(t, 1, mock.dialCalledTimes) require.Equal(t, 1, mock.loginCalledTimes) @@ -285,7 +285,7 @@ func TestMultiLDAP(t *testing.T) { t.Run("Should get only one user", func(t *testing.T) { mock := setup() - mock.usersFirstReturn = []*models.ExternalUserInfo{ + mock.usersFirstReturn = []*login.ExternalUserInfo{ { Login: "one", }, @@ -415,7 +415,7 @@ func TestMultiLDAP(t *testing.T) { t.Run("Should get users", func(t *testing.T) { mock := setup() - mock.usersFirstReturn = []*models.ExternalUserInfo{ + mock.usersFirstReturn = []*login.ExternalUserInfo{ { Login: "one", }, @@ -425,7 +425,7 @@ func TestMultiLDAP(t *testing.T) { }, } - mock.usersRestReturn = []*models.ExternalUserInfo{ + mock.usersRestReturn = []*login.ExternalUserInfo{ { Login: "three", }, @@ -461,23 +461,23 @@ type mockLDAP struct { dialErrReturn error loginErrReturn error - loginReturn *models.ExternalUserInfo + loginReturn *login.ExternalUserInfo bindErrReturn error usersErrReturn error - usersFirstReturn []*models.ExternalUserInfo - usersRestReturn []*models.ExternalUserInfo + usersFirstReturn []*login.ExternalUserInfo + usersRestReturn []*login.ExternalUserInfo } // Login test fn -func (mock *mockLDAP) Login(*models.LoginUserQuery) (*models.ExternalUserInfo, error) { +func (mock *mockLDAP) Login(*login.LoginUserQuery) (*login.ExternalUserInfo, error) { mock.loginCalledTimes++ return mock.loginReturn, mock.loginErrReturn } // Users test fn -func (mock *mockLDAP) Users([]string) ([]*models.ExternalUserInfo, error) { +func (mock *mockLDAP) Users([]string) ([]*login.ExternalUserInfo, error) { mock.usersCalledTimes++ if mock.usersCalledTimes == 1 { diff --git a/pkg/services/navtree/navtree.go b/pkg/services/navtree/navtree.go index 37cbe63c27d..8b641347547 100644 --- a/pkg/services/navtree/navtree.go +++ b/pkg/services/navtree/navtree.go @@ -1,10 +1,10 @@ package navtree import ( - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" pref "github.com/grafana/grafana/pkg/services/preference" ) type Service interface { - GetNavTree(c *models.ReqContext, hasEditPerm bool, prefs *pref.Preference) (*NavTreeRoot, error) + GetNavTree(c *contextmodel.ReqContext, hasEditPerm bool, prefs *pref.Preference) (*NavTreeRoot, error) } diff --git a/pkg/services/navtree/navtreeimpl/admin.go b/pkg/services/navtree/navtreeimpl/admin.go index 00c6cf09fde..9a565dc37d4 100644 --- a/pkg/services/navtree/navtreeimpl/admin.go +++ b/pkg/services/navtree/navtreeimpl/admin.go @@ -1,9 +1,9 @@ package navtreeimpl import ( - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" ac "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/correlations" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -12,7 +12,7 @@ import ( "github.com/grafana/grafana/pkg/services/serviceaccounts" ) -func (s *ServiceImpl) getOrgAdminNode(c *models.ReqContext) (*navtree.NavLink, error) { +func (s *ServiceImpl) getOrgAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink, error) { var configNodes []*navtree.NavLink hasAccess := ac.HasAccess(s.accessControl, c) @@ -119,7 +119,7 @@ func (s *ServiceImpl) getOrgAdminNode(c *models.ReqContext) (*navtree.NavLink, e return configNode, nil } -func (s *ServiceImpl) getServerAdminNode(c *models.ReqContext) *navtree.NavLink { +func (s *ServiceImpl) getServerAdminNode(c *contextmodel.ReqContext) *navtree.NavLink { hasAccess := ac.HasAccess(s.accessControl, c) hasGlobalAccess := ac.HasGlobalAccess(s.accessControl, s.accesscontrolService, c) orgsAccessEvaluator := ac.EvalPermission(ac.ActionOrgsRead) @@ -204,11 +204,11 @@ func (s *ServiceImpl) getServerAdminNode(c *models.ReqContext) *navtree.NavLink return adminNode } -func (s *ServiceImpl) ReqCanAdminTeams(c *models.ReqContext) bool { +func (s *ServiceImpl) ReqCanAdminTeams(c *contextmodel.ReqContext) bool { return c.OrgRole == org.RoleAdmin || (s.cfg.EditorsCanAdmin && c.OrgRole == org.RoleEditor) } -func enableServiceAccount(s *ServiceImpl, c *models.ReqContext) bool { +func enableServiceAccount(s *ServiceImpl, c *contextmodel.ReqContext) bool { hasAccess := ac.HasAccess(s.accessControl, c) return hasAccess(ac.ReqOrgAdmin, serviceaccounts.AccessEvaluator) } diff --git a/pkg/services/navtree/navtreeimpl/applinks.go b/pkg/services/navtree/navtreeimpl/applinks.go index dddffbcb5d0..d2bccdb4c56 100644 --- a/pkg/services/navtree/navtreeimpl/applinks.go +++ b/pkg/services/navtree/navtreeimpl/applinks.go @@ -5,16 +5,16 @@ import ( "sort" "strconv" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" ac "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/navtree" "github.com/grafana/grafana/pkg/services/pluginsettings" "github.com/grafana/grafana/pkg/util" ) -func (s *ServiceImpl) addAppLinks(treeRoot *navtree.NavTreeRoot, c *models.ReqContext) error { +func (s *ServiceImpl) addAppLinks(treeRoot *navtree.NavTreeRoot, c *contextmodel.ReqContext) error { topNavEnabled := s.features.IsEnabled(featuremgmt.FlagTopnav) hasAccess := ac.HasAccess(s.accessControl, c) appLinks := []*navtree.NavLink{} @@ -64,7 +64,7 @@ func (s *ServiceImpl) addAppLinks(treeRoot *navtree.NavTreeRoot, c *models.ReqCo return nil } -func (s *ServiceImpl) processAppPlugin(plugin plugins.PluginDTO, c *models.ReqContext, topNavEnabled bool, treeRoot *navtree.NavTreeRoot) *navtree.NavLink { +func (s *ServiceImpl) processAppPlugin(plugin plugins.PluginDTO, c *contextmodel.ReqContext, topNavEnabled bool, treeRoot *navtree.NavTreeRoot) *navtree.NavLink { hasAccessToInclude := s.hasAccessToInclude(c, plugin.ID) appLink := &navtree.NavLink{ Text: plugin.Name, @@ -176,7 +176,7 @@ func (s *ServiceImpl) processAppPlugin(plugin plugins.PluginDTO, c *models.ReqCo return nil } -func (s *ServiceImpl) addPluginToSection(c *models.ReqContext, treeRoot *navtree.NavTreeRoot, plugin plugins.PluginDTO, appLink *navtree.NavLink) { +func (s *ServiceImpl) addPluginToSection(c *contextmodel.ReqContext, treeRoot *navtree.NavTreeRoot, plugin plugins.PluginDTO, appLink *navtree.NavLink) { // Handle moving apps into specific navtree sections alertingNode := treeRoot.FindById(navtree.NavIDAlerting) sectionID := navtree.NavIDApps @@ -241,7 +241,7 @@ func (s *ServiceImpl) addPluginToSection(c *models.ReqContext, treeRoot *navtree } } -func (s *ServiceImpl) hasAccessToInclude(c *models.ReqContext, pluginID string) func(include *plugins.Includes) bool { +func (s *ServiceImpl) hasAccessToInclude(c *contextmodel.ReqContext, pluginID string) func(include *plugins.Includes) bool { hasAccess := ac.HasAccess(s.accessControl, c) return func(include *plugins.Includes) bool { useRBAC := s.features.IsEnabled(featuremgmt.FlagAccessControlOnCall) && diff --git a/pkg/services/navtree/navtreeimpl/applinks_test.go b/pkg/services/navtree/navtreeimpl/applinks_test.go index 79d39b287ca..32727630992 100644 --- a/pkg/services/navtree/navtreeimpl/applinks_test.go +++ b/pkg/services/navtree/navtreeimpl/applinks_test.go @@ -4,13 +4,15 @@ import ( "net/http" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models/roletype" "github.com/grafana/grafana/pkg/plugins" ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/navtree" @@ -18,12 +20,11 @@ import ( "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web" - "github.com/stretchr/testify/require" ) func TestAddAppLinks(t *testing.T) { httpReq, _ := http.NewRequest(http.MethodGet, "", nil) - reqCtx := &models.ReqContext{SignedInUser: &user.SignedInUser{}, Context: &web.Context{Req: httpReq}} + reqCtx := &contextmodel.ReqContext{SignedInUser: &user.SignedInUser{}, Context: &web.Context{Req: httpReq}} permissions := []ac.Permission{ {Action: plugins.ActionAppAccess, Scope: "*"}, {Action: plugins.ActionInstall, Scope: "*"}, @@ -388,7 +389,7 @@ func TestReadingNavigationSettings(t *testing.T) { func TestAddAppLinksAccessControl(t *testing.T) { httpReq, _ := http.NewRequest(http.MethodGet, "", nil) user := &user.SignedInUser{OrgID: 1} - reqCtx := &models.ReqContext{SignedInUser: user, Context: &web.Context{Req: httpReq}} + reqCtx := &contextmodel.ReqContext{SignedInUser: user, Context: &web.Context{Req: httpReq}} catalogReadAction := "test-app1.catalog:read" testApp1 := plugins.PluginDTO{ diff --git a/pkg/services/navtree/navtreeimpl/navtree.go b/pkg/services/navtree/navtreeimpl/navtree.go index 00740daa92c..47ca4bbafc9 100644 --- a/pkg/services/navtree/navtreeimpl/navtree.go +++ b/pkg/services/navtree/navtreeimpl/navtree.go @@ -7,10 +7,10 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/infra/kvstore" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/apikey" + 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" @@ -20,6 +20,7 @@ import ( pref "github.com/grafana/grafana/pkg/services/preference" "github.com/grafana/grafana/pkg/services/querylibrary" "github.com/grafana/grafana/pkg/services/star" + "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlesimpl" "github.com/grafana/grafana/pkg/setting" ) @@ -71,7 +72,7 @@ func ProvideService(cfg *setting.Cfg, accessControl ac.AccessControl, pluginStor } //nolint:gocyclo -func (s *ServiceImpl) GetNavTree(c *models.ReqContext, hasEditPerm bool, prefs *pref.Preference) (*navtree.NavTreeRoot, error) { +func (s *ServiceImpl) GetNavTree(c *contextmodel.ReqContext, hasEditPerm bool, prefs *pref.Preference) (*navtree.NavTreeRoot, error) { hasAccess := ac.HasAccess(s.accessControl, c) treeRoot := &navtree.NavTreeRoot{} @@ -111,7 +112,7 @@ func (s *ServiceImpl) GetNavTree(c *models.ReqContext, hasEditPerm bool, prefs * treeRoot.AddSection(dashboardLink) } - canExplore := func(context *models.ReqContext) bool { + canExplore := func(context *contextmodel.ReqContext) bool { return c.OrgRole == org.RoleAdmin || c.OrgRole == org.RoleEditor || setting.ViewersCanEdit } @@ -210,7 +211,7 @@ func (s *ServiceImpl) GetNavTree(c *models.ReqContext, hasEditPerm bool, prefs * return treeRoot, nil } -func (s *ServiceImpl) getHomeNode(c *models.ReqContext, prefs *pref.Preference) *navtree.NavLink { +func (s *ServiceImpl) getHomeNode(c *contextmodel.ReqContext, prefs *pref.Preference) *navtree.NavLink { homeUrl := s.cfg.AppSubURL + "/" homePage := s.cfg.HomePage @@ -232,23 +233,19 @@ func (s *ServiceImpl) getHomeNode(c *models.ReqContext, prefs *pref.Preference) return homeNode } -func (s *ServiceImpl) addHelpLinks(treeRoot *navtree.NavTreeRoot, c *models.ReqContext) { +func isSupportBundlesEnabled(s *ServiceImpl) bool { + return s.cfg.SectionWithEnvOverrides("support_bundles").Key("enabled").MustBool(false) && + s.features.IsEnabled(featuremgmt.FlagSupportBundles) +} + +func (s *ServiceImpl) addHelpLinks(treeRoot *navtree.NavTreeRoot, c *contextmodel.ReqContext) { if setting.HelpEnabled { helpVersion := fmt.Sprintf(`%s v%s (%s)`, setting.ApplicationName, setting.BuildVersion, setting.BuildCommit) if s.cfg.AnonymousHideVersion && !c.IsSignedIn { helpVersion = setting.ApplicationName } - supportBundleNode := &navtree.NavLink{ - Text: "Support bundles", - Id: "support-bundles", - Url: "/support-bundles", - Icon: "wrench", - Section: navtree.NavSectionConfig, - SortWeight: navtree.WeightHelp, - } - - treeRoot.AddSection(&navtree.NavLink{ + helpNode := &navtree.NavLink{ Text: "Help", SubTitle: helpVersion, Id: "help", @@ -256,12 +253,33 @@ func (s *ServiceImpl) addHelpLinks(treeRoot *navtree.NavTreeRoot, c *models.ReqC Icon: "question-circle", SortWeight: navtree.WeightHelp, Section: navtree.NavSectionConfig, - Children: []*navtree.NavLink{supportBundleNode}, - }) + Children: []*navtree.NavLink{}, + } + + treeRoot.AddSection(helpNode) + + hasAccess := ac.HasAccess(s.accessControl, c) + supportBundleAccess := ac.EvalAny( + ac.EvalPermission(supportbundlesimpl.ActionRead), + ac.EvalPermission(supportbundlesimpl.ActionCreate), + ) + + if isSupportBundlesEnabled(s) && hasAccess(ac.ReqGrafanaAdmin, supportBundleAccess) { + supportBundleNode := &navtree.NavLink{ + Text: "Support bundles", + Id: "support-bundles", + Url: "/support-bundles", + Icon: "wrench", + Section: navtree.NavSectionConfig, + SortWeight: navtree.WeightHelp, + } + + helpNode.Children = append(helpNode.Children, supportBundleNode) + } } } -func (s *ServiceImpl) getProfileNode(c *models.ReqContext) *navtree.NavLink { +func (s *ServiceImpl) getProfileNode(c *contextmodel.ReqContext) *navtree.NavLink { // Only set login if it's different from the name var login string if c.SignedInUser.Login != c.SignedInUser.NameOrFallback() { @@ -311,7 +329,7 @@ func (s *ServiceImpl) getProfileNode(c *models.ReqContext) *navtree.NavLink { } } -func (s *ServiceImpl) buildStarredItemsNavLinks(c *models.ReqContext) ([]*navtree.NavLink, error) { +func (s *ServiceImpl) buildStarredItemsNavLinks(c *contextmodel.ReqContext) ([]*navtree.NavLink, error) { starredItemsChildNavs := []*navtree.NavLink{} query := star.GetUserStarsQuery{ @@ -357,9 +375,9 @@ func (s *ServiceImpl) buildStarredItemsNavLinks(c *models.ReqContext) ([]*navtre return starredItemsChildNavs, nil } -func (s *ServiceImpl) buildDashboardNavLinks(c *models.ReqContext, hasEditPerm bool) []*navtree.NavLink { +func (s *ServiceImpl) buildDashboardNavLinks(c *contextmodel.ReqContext, hasEditPerm bool) []*navtree.NavLink { hasAccess := ac.HasAccess(s.accessControl, c) - hasEditPermInAnyFolder := func(c *models.ReqContext) bool { + hasEditPermInAnyFolder := func(c *contextmodel.ReqContext) bool { return hasEditPerm } @@ -446,7 +464,7 @@ func (s *ServiceImpl) buildDashboardNavLinks(c *models.ReqContext, hasEditPerm b return dashboardChildNavs } -func (s *ServiceImpl) buildLegacyAlertNavLinks(c *models.ReqContext) *navtree.NavLink { +func (s *ServiceImpl) buildLegacyAlertNavLinks(c *contextmodel.ReqContext) *navtree.NavLink { var alertChildNavs []*navtree.NavLink alertChildNavs = append(alertChildNavs, &navtree.NavLink{ Text: "Alert rules", Id: "alert-list", Url: s.cfg.AppSubURL + "/alerting/list", Icon: "list-ul", @@ -478,7 +496,7 @@ func (s *ServiceImpl) buildLegacyAlertNavLinks(c *models.ReqContext) *navtree.Na return &alertNav } -func (s *ServiceImpl) buildAlertNavLinks(c *models.ReqContext, hasEditPerm bool) *navtree.NavLink { +func (s *ServiceImpl) buildAlertNavLinks(c *contextmodel.ReqContext, hasEditPerm bool) *navtree.NavLink { hasAccess := ac.HasAccess(s.accessControl, c) var alertChildNavs []*navtree.NavLink @@ -499,7 +517,7 @@ func (s *ServiceImpl) buildAlertNavLinks(c *models.ReqContext, hasEditPerm bool) if hasAccess(ac.ReqOrgAdminOrEditor, ac.EvalAny(ac.EvalPermission(ac.ActionAlertingNotificationsRead), ac.EvalPermission(ac.ActionAlertingNotificationsExternalRead))) { alertChildNavs = append(alertChildNavs, &navtree.NavLink{ - Text: "Contact points", SubTitle: "Decide how your contacts are notified when an alert fires", Id: "receivers", Url: s.cfg.AppSubURL + "/alerting/notifications", + Text: "Contact points", SubTitle: "Choose how to notify your contact points when an alert instance fires", Id: "receivers", Url: s.cfg.AppSubURL + "/alerting/notifications", Icon: "comment-alt-share", }) alertChildNavs = append(alertChildNavs, &navtree.NavLink{Text: "Notification policies", SubTitle: "Determine how alerts are routed to contact points", Id: "am-routes", Url: s.cfg.AppSubURL + "/alerting/routes", Icon: "sitemap"}) @@ -517,7 +535,7 @@ func (s *ServiceImpl) buildAlertNavLinks(c *models.ReqContext, hasEditPerm bool) }) } - fallbackHasEditPerm := func(*models.ReqContext) bool { return hasEditPerm } + fallbackHasEditPerm := func(*contextmodel.ReqContext) bool { return hasEditPerm } if hasAccess(fallbackHasEditPerm, ac.EvalAny(ac.EvalPermission(ac.ActionAlertingRuleCreate), ac.EvalPermission(ac.ActionAlertingRuleExternalWrite))) { if !s.features.IsEnabled(featuremgmt.FlagTopnav) { @@ -527,7 +545,7 @@ func (s *ServiceImpl) buildAlertNavLinks(c *models.ReqContext, hasEditPerm bool) } alertChildNavs = append(alertChildNavs, &navtree.NavLink{ - Text: "New alert rule", SubTitle: "Create an alert rule", Id: "alert", + Text: "Create alert rule", SubTitle: "Create an alert rule", Id: "alert", Icon: "plus", Url: s.cfg.AppSubURL + "/alerting/new", HideFromTabs: true, ShowIconInNavbar: true, IsCreateAction: true, }) } @@ -555,7 +573,7 @@ func (s *ServiceImpl) buildAlertNavLinks(c *models.ReqContext, hasEditPerm bool) return nil } -func (s *ServiceImpl) buildDataConnectionsNavLink(c *models.ReqContext) *navtree.NavLink { +func (s *ServiceImpl) buildDataConnectionsNavLink(c *contextmodel.ReqContext) *navtree.NavLink { hasAccess := ac.HasAccess(s.accessControl, c) var children []*navtree.NavLink diff --git a/pkg/services/ngalert/api/api_alertmanager.go b/pkg/services/ngalert/api/api_alertmanager.go index 7b801795eeb..0bcf6efa641 100644 --- a/pkg/services/ngalert/api/api_alertmanager.go +++ b/pkg/services/ngalert/api/api_alertmanager.go @@ -14,8 +14,8 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/notifier" "github.com/grafana/grafana/pkg/services/ngalert/store" @@ -42,7 +42,7 @@ func (e UnknownReceiverError) Error() string { return fmt.Sprintf("unknown receiver: %s", e.UID) } -func (srv AlertmanagerSrv) RouteGetAMStatus(c *models.ReqContext) response.Response { +func (srv AlertmanagerSrv) RouteGetAMStatus(c *contextmodel.ReqContext) response.Response { am, errResp := srv.AlertmanagerFor(c.OrgID) if errResp != nil { return errResp @@ -51,7 +51,7 @@ func (srv AlertmanagerSrv) RouteGetAMStatus(c *models.ReqContext) response.Respo return response.JSON(http.StatusOK, am.GetStatus()) } -func (srv AlertmanagerSrv) RouteCreateSilence(c *models.ReqContext, postableSilence apimodels.PostableSilence) response.Response { +func (srv AlertmanagerSrv) RouteCreateSilence(c *contextmodel.ReqContext, postableSilence apimodels.PostableSilence) response.Response { err := postableSilence.Validate(strfmt.Default) if err != nil { srv.log.Error("silence failed validation", "error", err) @@ -92,7 +92,7 @@ func (srv AlertmanagerSrv) RouteCreateSilence(c *models.ReqContext, postableSile }) } -func (srv AlertmanagerSrv) RouteDeleteAlertingConfig(c *models.ReqContext) response.Response { +func (srv AlertmanagerSrv) RouteDeleteAlertingConfig(c *contextmodel.ReqContext) response.Response { am, errResp := srv.AlertmanagerFor(c.OrgID) if errResp != nil { return errResp @@ -106,7 +106,7 @@ func (srv AlertmanagerSrv) RouteDeleteAlertingConfig(c *models.ReqContext) respo return response.JSON(http.StatusAccepted, util.DynMap{"message": "configuration deleted; the default is applied"}) } -func (srv AlertmanagerSrv) RouteDeleteSilence(c *models.ReqContext, silenceID string) response.Response { +func (srv AlertmanagerSrv) RouteDeleteSilence(c *contextmodel.ReqContext, silenceID string) response.Response { am, errResp := srv.AlertmanagerFor(c.OrgID) if errResp != nil { return errResp @@ -121,7 +121,7 @@ func (srv AlertmanagerSrv) RouteDeleteSilence(c *models.ReqContext, silenceID st return response.JSON(http.StatusOK, util.DynMap{"message": "silence deleted"}) } -func (srv AlertmanagerSrv) RouteGetAlertingConfig(c *models.ReqContext) response.Response { +func (srv AlertmanagerSrv) RouteGetAlertingConfig(c *contextmodel.ReqContext) response.Response { config, err := srv.mam.GetAlertmanagerConfiguration(c.Req.Context(), c.OrgID) if err != nil { if errors.Is(err, store.ErrNoAlertmanagerConfiguration) { @@ -132,7 +132,7 @@ func (srv AlertmanagerSrv) RouteGetAlertingConfig(c *models.ReqContext) response return response.JSON(http.StatusOK, config) } -func (srv AlertmanagerSrv) RouteGetAMAlertGroups(c *models.ReqContext) response.Response { +func (srv AlertmanagerSrv) RouteGetAMAlertGroups(c *contextmodel.ReqContext) response.Response { am, errResp := srv.AlertmanagerFor(c.OrgID) if errResp != nil { return errResp @@ -156,7 +156,7 @@ func (srv AlertmanagerSrv) RouteGetAMAlertGroups(c *models.ReqContext) response. return response.JSON(http.StatusOK, groups) } -func (srv AlertmanagerSrv) RouteGetAMAlerts(c *models.ReqContext) response.Response { +func (srv AlertmanagerSrv) RouteGetAMAlerts(c *contextmodel.ReqContext) response.Response { am, errResp := srv.AlertmanagerFor(c.OrgID) if errResp != nil { return errResp @@ -183,7 +183,7 @@ func (srv AlertmanagerSrv) RouteGetAMAlerts(c *models.ReqContext) response.Respo return response.JSON(http.StatusOK, alerts) } -func (srv AlertmanagerSrv) RouteGetSilence(c *models.ReqContext, silenceID string) response.Response { +func (srv AlertmanagerSrv) RouteGetSilence(c *contextmodel.ReqContext, silenceID string) response.Response { am, errResp := srv.AlertmanagerFor(c.OrgID) if errResp != nil { return errResp @@ -200,7 +200,7 @@ func (srv AlertmanagerSrv) RouteGetSilence(c *models.ReqContext, silenceID strin return response.JSON(http.StatusOK, gettableSilence) } -func (srv AlertmanagerSrv) RouteGetSilences(c *models.ReqContext) response.Response { +func (srv AlertmanagerSrv) RouteGetSilences(c *contextmodel.ReqContext) response.Response { am, errResp := srv.AlertmanagerFor(c.OrgID) if errResp != nil { return errResp @@ -217,7 +217,7 @@ func (srv AlertmanagerSrv) RouteGetSilences(c *models.ReqContext) response.Respo return response.JSON(http.StatusOK, gettableSilences) } -func (srv AlertmanagerSrv) RoutePostAlertingConfig(c *models.ReqContext, body apimodels.PostableUserConfig) response.Response { +func (srv AlertmanagerSrv) RoutePostAlertingConfig(c *contextmodel.ReqContext, body apimodels.PostableUserConfig) response.Response { currentConfig, err := srv.mam.GetAlertmanagerConfiguration(c.Req.Context(), c.OrgID) // If a config is present and valid we proceed with the guard, otherwise we // just bypass the guard which is okay as we are anyway in an invalid state. @@ -248,7 +248,7 @@ func (srv AlertmanagerSrv) RoutePostAlertingConfig(c *models.ReqContext, body ap return ErrResp(http.StatusInternalServerError, err, "") } -func (srv AlertmanagerSrv) RouteGetReceivers(c *models.ReqContext) response.Response { +func (srv AlertmanagerSrv) RouteGetReceivers(c *contextmodel.ReqContext) response.Response { am, errResp := srv.AlertmanagerFor(c.OrgID) if errResp != nil { return errResp @@ -258,7 +258,7 @@ func (srv AlertmanagerSrv) RouteGetReceivers(c *models.ReqContext) response.Resp return response.JSON(http.StatusOK, rcvs) } -func (srv AlertmanagerSrv) RoutePostTestReceivers(c *models.ReqContext, body apimodels.TestReceiversConfigBodyParams) response.Response { +func (srv AlertmanagerSrv) RoutePostTestReceivers(c *contextmodel.ReqContext, body apimodels.TestReceiversConfigBodyParams) response.Response { if err := srv.crypto.LoadSecureSettings(c.Req.Context(), c.OrgID, body.Receivers); err != nil { var unknownReceiverError UnknownReceiverError if errors.As(err, &unknownReceiverError) { diff --git a/pkg/services/ngalert/api/api_alertmanager_test.go b/pkg/services/ngalert/api/api_alertmanager_test.go index 0553775fc66..ce9cb47d4b2 100644 --- a/pkg/services/ngalert/api/api_alertmanager_test.go +++ b/pkg/services/ngalert/api/api_alertmanager_test.go @@ -16,9 +16,9 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" acMock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/metrics" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" @@ -166,7 +166,7 @@ func TestAlertmanagerConfig(t *testing.T) { sut := createSut(t, nil) t.Run("assert 404 Not Found when applying config to nonexistent org", func(t *testing.T) { - rc := models.ReqContext{ + rc := contextmodel.ReqContext{ Context: &web.Context{ Req: &http.Request{}, }, @@ -183,7 +183,7 @@ func TestAlertmanagerConfig(t *testing.T) { }) t.Run("assert 202 when config successfully applied", func(t *testing.T) { - rc := models.ReqContext{ + rc := contextmodel.ReqContext{ Context: &web.Context{ Req: &http.Request{}, }, @@ -200,7 +200,7 @@ func TestAlertmanagerConfig(t *testing.T) { t.Run("assert 202 when alertmanager to configure is not ready", func(t *testing.T) { sut := createSut(t, nil) - rc := models.ReqContext{ + rc := contextmodel.ReqContext{ Context: &web.Context{ Req: &http.Request{}, }, @@ -330,7 +330,7 @@ func TestSilenceCreate(t *testing.T) { for _, cas := range cases { t.Run(cas.name, func(t *testing.T) { - rc := models.ReqContext{ + rc := contextmodel.ReqContext{ Context: &web.Context{ Req: &http.Request{}, }, @@ -456,7 +456,7 @@ func TestRouteCreateSilence(t *testing.T) { ac := tesCase.accessControl() sut := createSut(t, ac) - rc := models.ReqContext{ + rc := contextmodel.ReqContext{ Context: &web.Context{ Req: &http.Request{}, }, @@ -622,8 +622,8 @@ func withEmptyID(silence *apimodels.PostableSilence) { silence.ID = "" } -func createRequestCtxInOrg(org int64) *models.ReqContext { - return &models.ReqContext{ +func createRequestCtxInOrg(org int64) *contextmodel.ReqContext { + return &contextmodel.ReqContext{ Context: &web.Context{ Req: &http.Request{}, }, diff --git a/pkg/services/ngalert/api/api_configuration.go b/pkg/services/ngalert/api/api_configuration.go index 4d78029bd64..cbf2cf16ba1 100644 --- a/pkg/services/ngalert/api/api_configuration.go +++ b/pkg/services/ngalert/api/api_configuration.go @@ -6,17 +6,17 @@ import ( "fmt" "net/http" + v1 "github.com/prometheus/client_golang/api/prometheus/v1" + "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/util" - - v1 "github.com/prometheus/client_golang/api/prometheus/v1" ) type ConfigSrv struct { @@ -26,7 +26,7 @@ type ConfigSrv struct { log log.Logger } -func (srv ConfigSrv) RouteGetAlertmanagers(c *models.ReqContext) response.Response { +func (srv ConfigSrv) RouteGetAlertmanagers(c *contextmodel.ReqContext) response.Response { urls := srv.alertmanagerProvider.AlertmanagersFor(c.OrgID) droppedURLs := srv.alertmanagerProvider.DroppedAlertmanagersFor(c.OrgID) ams := v1.AlertManagersResult{Active: make([]v1.AlertManager, len(urls)), Dropped: make([]v1.AlertManager, len(droppedURLs))} @@ -43,7 +43,7 @@ func (srv ConfigSrv) RouteGetAlertmanagers(c *models.ReqContext) response.Respon }) } -func (srv ConfigSrv) RouteGetNGalertConfig(c *models.ReqContext) response.Response { +func (srv ConfigSrv) RouteGetNGalertConfig(c *contextmodel.ReqContext) response.Response { if c.OrgRole != org.RoleAdmin { return accessForbiddenResp() } @@ -65,7 +65,7 @@ func (srv ConfigSrv) RouteGetNGalertConfig(c *models.ReqContext) response.Respon return response.JSON(http.StatusOK, resp) } -func (srv ConfigSrv) RoutePostNGalertConfig(c *models.ReqContext, body apimodels.PostableNGalertConfig) response.Response { +func (srv ConfigSrv) RoutePostNGalertConfig(c *contextmodel.ReqContext, body apimodels.PostableNGalertConfig) response.Response { if c.OrgRole != org.RoleAdmin { return accessForbiddenResp() } @@ -99,7 +99,7 @@ func (srv ConfigSrv) RoutePostNGalertConfig(c *models.ReqContext, body apimodels return response.JSON(http.StatusCreated, util.DynMap{"message": "admin configuration updated"}) } -func (srv ConfigSrv) RouteDeleteNGalertConfig(c *models.ReqContext) response.Response { +func (srv ConfigSrv) RouteDeleteNGalertConfig(c *contextmodel.ReqContext) response.Response { if c.OrgRole != org.RoleAdmin { return accessForbiddenResp() } @@ -135,7 +135,7 @@ func (srv ConfigSrv) externalAlertmanagers(ctx context.Context, orgID int64) ([] return alertmanagers, nil } -func (srv ConfigSrv) RouteGetAlertingStatus(c *models.ReqContext) response.Response { +func (srv ConfigSrv) RouteGetAlertingStatus(c *contextmodel.ReqContext) response.Response { sendsAlertsTo := ngmodels.InternalAlertmanager cfg, err := srv.store.GetAdminConfiguration(c.OrgID) diff --git a/pkg/services/ngalert/api/api_configuration_test.go b/pkg/services/ngalert/api/api_configuration_test.go index df24489f311..f4470d93e17 100644 --- a/pkg/services/ngalert/api/api_configuration_test.go +++ b/pkg/services/ngalert/api/api_configuration_test.go @@ -5,13 +5,14 @@ import ( "net/http" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/services/datasources" fakeDatasources "github.com/grafana/grafana/pkg/services/datasources/fakes" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/services/org" - "github.com/stretchr/testify/require" ) func TestExternalAlertmanagerChoice(t *testing.T) { diff --git a/pkg/services/ngalert/api/api_prometheus.go b/pkg/services/ngalert/api/api_prometheus.go index c894258bf20..3c9a1350640 100644 --- a/pkg/services/ngalert/api/api_prometheus.go +++ b/pkg/services/ngalert/api/api_prometheus.go @@ -10,17 +10,17 @@ import ( "strings" "time" + apiv1 "github.com/prometheus/client_golang/api/prometheus/v1" + "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/folder" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/eval" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/state" - - apiv1 "github.com/prometheus/client_golang/api/prometheus/v1" ) type PrometheusSrv struct { @@ -32,7 +32,7 @@ type PrometheusSrv struct { const queryIncludeInternalLabels = "includeInternalLabels" -func (srv PrometheusSrv) RouteGetAlertStatuses(c *models.ReqContext) response.Response { +func (srv PrometheusSrv) RouteGetAlertStatuses(c *contextmodel.ReqContext) response.Response { alertResponse := apimodels.AlertResponse{ DiscoveryBase: apimodels.DiscoveryBase{ Status: "success", @@ -105,7 +105,7 @@ func getPanelIDFromRequest(r *http.Request) (int64, error) { return 0, nil } -func (srv PrometheusSrv) RouteGetRuleStatuses(c *models.ReqContext) response.Response { +func (srv PrometheusSrv) RouteGetRuleStatuses(c *contextmodel.ReqContext) response.Response { dashboardUID := c.Query("dashboard_uid") panelID, err := getPanelIDFromRequest(c.Req) if err != nil { diff --git a/pkg/services/ngalert/api/api_prometheus_test.go b/pkg/services/ngalert/api/api_prometheus_test.go index 4bf06b8b4a4..9d7599027a0 100644 --- a/pkg/services/ngalert/api/api_prometheus_test.go +++ b/pkg/services/ngalert/api/api_prometheus_test.go @@ -9,14 +9,14 @@ import ( "testing" "time" + alertingModels "github.com/grafana/alerting/alerting/models" "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - alertingModels "github.com/grafana/alerting/alerting/models" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/eval" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" @@ -93,7 +93,7 @@ func TestRouteGetAlertStatuses(t *testing.T) { _, _, _, api := setupAPI(t) req, err := http.NewRequest("GET", "/api/v1/alerts", nil) require.NoError(t, err) - c := &models.ReqContext{Context: &web.Context{Req: req}, SignedInUser: &user.SignedInUser{OrgID: orgID}} + c := &contextmodel.ReqContext{Context: &web.Context{Req: req}, SignedInUser: &user.SignedInUser{OrgID: orgID}} r := api.RouteGetAlertStatuses(c) require.Equal(t, http.StatusOK, r.Status()) @@ -112,7 +112,7 @@ func TestRouteGetAlertStatuses(t *testing.T) { fakeAIM.GenerateAlertInstances(1, util.GenerateShortUID(), 2) req, err := http.NewRequest("GET", "/api/v1/alerts", nil) require.NoError(t, err) - c := &models.ReqContext{Context: &web.Context{Req: req}, SignedInUser: &user.SignedInUser{OrgID: orgID}} + c := &contextmodel.ReqContext{Context: &web.Context{Req: req}, SignedInUser: &user.SignedInUser{OrgID: orgID}} r := api.RouteGetAlertStatuses(c) require.Equal(t, http.StatusOK, r.Status()) @@ -154,7 +154,7 @@ func TestRouteGetAlertStatuses(t *testing.T) { fakeAIM.GenerateAlertInstances(1, util.GenerateShortUID(), 2, withAlertingState()) req, err := http.NewRequest("GET", "/api/v1/alerts", nil) require.NoError(t, err) - c := &models.ReqContext{Context: &web.Context{Req: req}, SignedInUser: &user.SignedInUser{OrgID: orgID}} + c := &contextmodel.ReqContext{Context: &web.Context{Req: req}, SignedInUser: &user.SignedInUser{OrgID: orgID}} r := api.RouteGetAlertStatuses(c) require.Equal(t, http.StatusOK, r.Status()) @@ -196,7 +196,7 @@ func TestRouteGetAlertStatuses(t *testing.T) { fakeAIM.GenerateAlertInstances(orgID, util.GenerateShortUID(), 2) req, err := http.NewRequest("GET", "/api/v1/alerts?includeInternalLabels=true", nil) require.NoError(t, err) - c := &models.ReqContext{Context: &web.Context{Req: req}, SignedInUser: &user.SignedInUser{OrgID: orgID}} + c := &contextmodel.ReqContext{Context: &web.Context{Req: req}, SignedInUser: &user.SignedInUser{OrgID: orgID}} r := api.RouteGetAlertStatuses(c) require.Equal(t, http.StatusOK, r.Status()) @@ -258,7 +258,7 @@ func TestRouteGetRuleStatuses(t *testing.T) { req, err := http.NewRequest("GET", "/api/v1/rules", nil) require.NoError(t, err) - c := &models.ReqContext{Context: &web.Context{Req: req}, SignedInUser: &user.SignedInUser{OrgID: orgID, OrgRole: org.RoleViewer}} + c := &contextmodel.ReqContext{Context: &web.Context{Req: req}, SignedInUser: &user.SignedInUser{OrgID: orgID, OrgRole: org.RoleViewer}} t.Run("with no rules", func(t *testing.T) { _, _, _, api := setupAPI(t) @@ -328,7 +328,7 @@ func TestRouteGetRuleStatuses(t *testing.T) { req, err := http.NewRequest("GET", "/api/v1/rules?includeInternalLabels=true", nil) require.NoError(t, err) - c := &models.ReqContext{Context: &web.Context{Req: req}, SignedInUser: &user.SignedInUser{OrgID: orgID, OrgRole: org.RoleViewer}} + c := &contextmodel.ReqContext{Context: &web.Context{Req: req}, SignedInUser: &user.SignedInUser{OrgID: orgID, OrgRole: org.RoleViewer}} r := api.RouteGetRuleStatuses(c) require.Equal(t, http.StatusOK, r.Status()) diff --git a/pkg/services/ngalert/api/api_provisioning.go b/pkg/services/ngalert/api/api_provisioning.go index 748b78106aa..93c3e3696bf 100644 --- a/pkg/services/ngalert/api/api_provisioning.go +++ b/pkg/services/ngalert/api/api_provisioning.go @@ -3,15 +3,18 @@ package api import ( "context" "errors" + "fmt" "net/http" + "strings" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" alerting_models "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/provisioning" "github.com/grafana/grafana/pkg/services/ngalert/store" + "github.com/grafana/grafana/pkg/services/provisioning/alerting/file" "github.com/grafana/grafana/pkg/util" ) @@ -60,9 +63,12 @@ type AlertRuleService interface { DeleteAlertRule(ctx context.Context, orgID int64, ruleUID string, provenance alerting_models.Provenance) error GetRuleGroup(ctx context.Context, orgID int64, folder, group string) (alerting_models.AlertRuleGroup, error) ReplaceRuleGroup(ctx context.Context, orgID int64, group alerting_models.AlertRuleGroup, userID int64, provenance alerting_models.Provenance) error + GetAlertRuleWithFolderTitle(ctx context.Context, orgID int64, ruleUID string) (provisioning.AlertRuleWithFolderTitle, error) + GetAlertRuleGroupWithFolderTitle(ctx context.Context, orgID int64, folder, group string) (file.AlertRuleGroupWithFolderTitle, error) + GetAlertGroupsWithFolderTitle(ctx context.Context, orgID int64) ([]file.AlertRuleGroupWithFolderTitle, error) } -func (srv *ProvisioningSrv) RouteGetPolicyTree(c *models.ReqContext) response.Response { +func (srv *ProvisioningSrv) RouteGetPolicyTree(c *contextmodel.ReqContext) response.Response { policies, err := srv.policies.GetPolicyTree(c.Req.Context(), c.OrgID) if errors.Is(err, store.ErrNoAlertmanagerConfiguration) { return ErrResp(http.StatusNotFound, err, "") @@ -74,7 +80,7 @@ func (srv *ProvisioningSrv) RouteGetPolicyTree(c *models.ReqContext) response.Re return response.JSON(http.StatusOK, policies) } -func (srv *ProvisioningSrv) RoutePutPolicyTree(c *models.ReqContext, tree definitions.Route) response.Response { +func (srv *ProvisioningSrv) RoutePutPolicyTree(c *contextmodel.ReqContext, tree definitions.Route) response.Response { err := srv.policies.UpdatePolicyTree(c.Req.Context(), c.OrgID, tree, alerting_models.ProvenanceAPI) if errors.Is(err, store.ErrNoAlertmanagerConfiguration) { return ErrResp(http.StatusNotFound, err, "") @@ -89,7 +95,7 @@ func (srv *ProvisioningSrv) RoutePutPolicyTree(c *models.ReqContext, tree defini return response.JSON(http.StatusAccepted, util.DynMap{"message": "policies updated"}) } -func (srv *ProvisioningSrv) RouteResetPolicyTree(c *models.ReqContext) response.Response { +func (srv *ProvisioningSrv) RouteResetPolicyTree(c *contextmodel.ReqContext) response.Response { tree, err := srv.policies.ResetPolicyTree(c.Req.Context(), c.OrgID) if err != nil { return ErrResp(http.StatusInternalServerError, err, "") @@ -97,7 +103,7 @@ func (srv *ProvisioningSrv) RouteResetPolicyTree(c *models.ReqContext) response. return response.JSON(http.StatusAccepted, tree) } -func (srv *ProvisioningSrv) RouteGetContactPoints(c *models.ReqContext) response.Response { +func (srv *ProvisioningSrv) RouteGetContactPoints(c *contextmodel.ReqContext) response.Response { q := provisioning.ContactPointQuery{ Name: c.Query("name"), OrgID: c.OrgID, @@ -109,7 +115,7 @@ func (srv *ProvisioningSrv) RouteGetContactPoints(c *models.ReqContext) response return response.JSON(http.StatusOK, cps) } -func (srv *ProvisioningSrv) RoutePostContactPoint(c *models.ReqContext, cp definitions.EmbeddedContactPoint) response.Response { +func (srv *ProvisioningSrv) RoutePostContactPoint(c *contextmodel.ReqContext, cp definitions.EmbeddedContactPoint) response.Response { // TODO: provenance is hardcoded for now, change it later to make it more flexible contactPoint, err := srv.contactPointService.CreateContactPoint(c.Req.Context(), c.OrgID, cp, alerting_models.ProvenanceAPI) if errors.Is(err, provisioning.ErrValidation) { @@ -121,7 +127,7 @@ func (srv *ProvisioningSrv) RoutePostContactPoint(c *models.ReqContext, cp defin return response.JSON(http.StatusAccepted, contactPoint) } -func (srv *ProvisioningSrv) RoutePutContactPoint(c *models.ReqContext, cp definitions.EmbeddedContactPoint, UID string) response.Response { +func (srv *ProvisioningSrv) RoutePutContactPoint(c *contextmodel.ReqContext, cp definitions.EmbeddedContactPoint, UID string) response.Response { cp.UID = UID err := srv.contactPointService.UpdateContactPoint(c.Req.Context(), c.OrgID, cp, alerting_models.ProvenanceAPI) if errors.Is(err, provisioning.ErrValidation) { @@ -136,7 +142,7 @@ func (srv *ProvisioningSrv) RoutePutContactPoint(c *models.ReqContext, cp defini return response.JSON(http.StatusAccepted, util.DynMap{"message": "contactpoint updated"}) } -func (srv *ProvisioningSrv) RouteDeleteContactPoint(c *models.ReqContext, UID string) response.Response { +func (srv *ProvisioningSrv) RouteDeleteContactPoint(c *contextmodel.ReqContext, UID string) response.Response { err := srv.contactPointService.DeleteContactPoint(c.Req.Context(), c.OrgID, UID) if err != nil { return ErrResp(http.StatusInternalServerError, err, "") @@ -144,7 +150,7 @@ func (srv *ProvisioningSrv) RouteDeleteContactPoint(c *models.ReqContext, UID st return response.JSON(http.StatusAccepted, util.DynMap{"message": "contactpoint deleted"}) } -func (srv *ProvisioningSrv) RouteGetTemplates(c *models.ReqContext) response.Response { +func (srv *ProvisioningSrv) RouteGetTemplates(c *contextmodel.ReqContext) response.Response { templates, err := srv.templates.GetTemplates(c.Req.Context(), c.OrgID) if err != nil { return ErrResp(http.StatusInternalServerError, err, "") @@ -156,7 +162,7 @@ func (srv *ProvisioningSrv) RouteGetTemplates(c *models.ReqContext) response.Res return response.JSON(http.StatusOK, result) } -func (srv *ProvisioningSrv) RouteGetTemplate(c *models.ReqContext, name string) response.Response { +func (srv *ProvisioningSrv) RouteGetTemplate(c *contextmodel.ReqContext, name string) response.Response { templates, err := srv.templates.GetTemplates(c.Req.Context(), c.OrgID) if err != nil { return ErrResp(http.StatusInternalServerError, err, "") @@ -167,7 +173,7 @@ func (srv *ProvisioningSrv) RouteGetTemplate(c *models.ReqContext, name string) return response.Empty(http.StatusNotFound) } -func (srv *ProvisioningSrv) RoutePutTemplate(c *models.ReqContext, body definitions.NotificationTemplateContent, name string) response.Response { +func (srv *ProvisioningSrv) RoutePutTemplate(c *contextmodel.ReqContext, body definitions.NotificationTemplateContent, name string) response.Response { tmpl := definitions.NotificationTemplate{ Name: name, Template: body.Template, @@ -183,7 +189,7 @@ func (srv *ProvisioningSrv) RoutePutTemplate(c *models.ReqContext, body definiti return response.JSON(http.StatusAccepted, modified) } -func (srv *ProvisioningSrv) RouteDeleteTemplate(c *models.ReqContext, name string) response.Response { +func (srv *ProvisioningSrv) RouteDeleteTemplate(c *contextmodel.ReqContext, name string) response.Response { err := srv.templates.DeleteTemplate(c.Req.Context(), c.OrgID, name) if err != nil { return ErrResp(http.StatusInternalServerError, err, "") @@ -191,7 +197,7 @@ func (srv *ProvisioningSrv) RouteDeleteTemplate(c *models.ReqContext, name strin return response.JSON(http.StatusNoContent, nil) } -func (srv *ProvisioningSrv) RouteGetMuteTiming(c *models.ReqContext, name string) response.Response { +func (srv *ProvisioningSrv) RouteGetMuteTiming(c *contextmodel.ReqContext, name string) response.Response { timings, err := srv.muteTimings.GetMuteTimings(c.Req.Context(), c.OrgID) if err != nil { return ErrResp(http.StatusInternalServerError, err, "") @@ -204,7 +210,7 @@ func (srv *ProvisioningSrv) RouteGetMuteTiming(c *models.ReqContext, name string return response.Empty(http.StatusNotFound) } -func (srv *ProvisioningSrv) RouteGetMuteTimings(c *models.ReqContext) response.Response { +func (srv *ProvisioningSrv) RouteGetMuteTimings(c *contextmodel.ReqContext) response.Response { timings, err := srv.muteTimings.GetMuteTimings(c.Req.Context(), c.OrgID) if err != nil { return ErrResp(http.StatusInternalServerError, err, "") @@ -212,7 +218,7 @@ func (srv *ProvisioningSrv) RouteGetMuteTimings(c *models.ReqContext) response.R return response.JSON(http.StatusOK, timings) } -func (srv *ProvisioningSrv) RoutePostMuteTiming(c *models.ReqContext, mt definitions.MuteTimeInterval) response.Response { +func (srv *ProvisioningSrv) RoutePostMuteTiming(c *contextmodel.ReqContext, mt definitions.MuteTimeInterval) response.Response { mt.Provenance = alerting_models.ProvenanceAPI created, err := srv.muteTimings.CreateMuteTiming(c.Req.Context(), mt, c.OrgID) if err != nil { @@ -224,7 +230,7 @@ func (srv *ProvisioningSrv) RoutePostMuteTiming(c *models.ReqContext, mt definit return response.JSON(http.StatusCreated, created) } -func (srv *ProvisioningSrv) RoutePutMuteTiming(c *models.ReqContext, mt definitions.MuteTimeInterval, name string) response.Response { +func (srv *ProvisioningSrv) RoutePutMuteTiming(c *contextmodel.ReqContext, mt definitions.MuteTimeInterval, name string) response.Response { mt.Name = name mt.Provenance = alerting_models.ProvenanceAPI updated, err := srv.muteTimings.UpdateMuteTiming(c.Req.Context(), mt, c.OrgID) @@ -240,7 +246,7 @@ func (srv *ProvisioningSrv) RoutePutMuteTiming(c *models.ReqContext, mt definiti return response.JSON(http.StatusAccepted, updated) } -func (srv *ProvisioningSrv) RouteDeleteMuteTiming(c *models.ReqContext, name string) response.Response { +func (srv *ProvisioningSrv) RouteDeleteMuteTiming(c *contextmodel.ReqContext, name string) response.Response { err := srv.muteTimings.DeleteMuteTiming(c.Req.Context(), name, c.OrgID) if err != nil { return ErrResp(http.StatusInternalServerError, err, "") @@ -248,7 +254,7 @@ func (srv *ProvisioningSrv) RouteDeleteMuteTiming(c *models.ReqContext, name str return response.JSON(http.StatusNoContent, nil) } -func (srv *ProvisioningSrv) RouteGetAlertRules(c *models.ReqContext) response.Response { +func (srv *ProvisioningSrv) RouteGetAlertRules(c *contextmodel.ReqContext) response.Response { rules, err := srv.alertRules.GetAlertRules(c.Req.Context(), c.OrgID) if err != nil { return ErrResp(http.StatusInternalServerError, err, "") @@ -256,7 +262,7 @@ func (srv *ProvisioningSrv) RouteGetAlertRules(c *models.ReqContext) response.Re return response.JSON(http.StatusOK, definitions.NewAlertRules(rules)) } -func (srv *ProvisioningSrv) RouteRouteGetAlertRule(c *models.ReqContext, UID string) response.Response { +func (srv *ProvisioningSrv) RouteRouteGetAlertRule(c *contextmodel.ReqContext, UID string) response.Response { rule, provenace, err := srv.alertRules.GetAlertRule(c.Req.Context(), c.OrgID, UID) if err != nil { return ErrResp(http.StatusInternalServerError, err, "") @@ -264,7 +270,7 @@ func (srv *ProvisioningSrv) RouteRouteGetAlertRule(c *models.ReqContext, UID str return response.JSON(http.StatusOK, definitions.NewAlertRule(rule, provenace)) } -func (srv *ProvisioningSrv) RoutePostAlertRule(c *models.ReqContext, ar definitions.ProvisionedAlertRule) response.Response { +func (srv *ProvisioningSrv) RoutePostAlertRule(c *contextmodel.ReqContext, ar definitions.ProvisionedAlertRule) response.Response { upstreamModel, err := ar.UpstreamModel() upstreamModel.OrgID = c.OrgID if err != nil { @@ -289,7 +295,7 @@ func (srv *ProvisioningSrv) RoutePostAlertRule(c *models.ReqContext, ar definiti return response.JSON(http.StatusCreated, resp) } -func (srv *ProvisioningSrv) RoutePutAlertRule(c *models.ReqContext, ar definitions.ProvisionedAlertRule, UID string) response.Response { +func (srv *ProvisioningSrv) RoutePutAlertRule(c *contextmodel.ReqContext, ar definitions.ProvisionedAlertRule, UID string) response.Response { updated, err := ar.UpstreamModel() if err != nil { ErrResp(http.StatusBadRequest, err, "") @@ -315,7 +321,7 @@ func (srv *ProvisioningSrv) RoutePutAlertRule(c *models.ReqContext, ar definitio return response.JSON(http.StatusOK, resp) } -func (srv *ProvisioningSrv) RouteDeleteAlertRule(c *models.ReqContext, UID string) response.Response { +func (srv *ProvisioningSrv) RouteDeleteAlertRule(c *contextmodel.ReqContext, UID string) response.Response { err := srv.alertRules.DeleteAlertRule(c.Req.Context(), c.OrgID, UID, alerting_models.ProvenanceAPI) if err != nil { return ErrResp(http.StatusInternalServerError, err, "") @@ -323,7 +329,7 @@ func (srv *ProvisioningSrv) RouteDeleteAlertRule(c *models.ReqContext, UID strin return response.JSON(http.StatusNoContent, "") } -func (srv *ProvisioningSrv) RouteGetAlertRuleGroup(c *models.ReqContext, folder string, group string) response.Response { +func (srv *ProvisioningSrv) RouteGetAlertRuleGroup(c *contextmodel.ReqContext, folder string, group string) response.Response { g, err := srv.alertRules.GetRuleGroup(c.Req.Context(), c.OrgID, folder, group) if err != nil { if errors.Is(err, store.ErrAlertRuleGroupNotFound) { @@ -334,7 +340,67 @@ func (srv *ProvisioningSrv) RouteGetAlertRuleGroup(c *models.ReqContext, folder return response.JSON(http.StatusOK, definitions.NewAlertRuleGroupFromModel(g)) } -func (srv *ProvisioningSrv) RoutePutAlertRuleGroup(c *models.ReqContext, ag definitions.AlertRuleGroup, folderUID string, group string) response.Response { +// RouteGetAlertRulesExport retrieves all alert rules in a format compatible with file provisioning. +func (srv *ProvisioningSrv) RouteGetAlertRulesExport(c *contextmodel.ReqContext) response.Response { + groupsWithTitle, err := srv.alertRules.GetAlertGroupsWithFolderTitle(c.Req.Context(), c.OrgID) + if err != nil { + return ErrResp(http.StatusInternalServerError, err, "failed to get alert rules") + } + + e, err := file.NewAlertingFileExport(groupsWithTitle) + if err != nil { + return ErrResp(http.StatusInternalServerError, err, "failed to create alerting file export") + } + + return exportResponse(c, e) +} + +// RouteGetAlertRuleGroupExport retrieves the given alert rule group in a format compatible with file provisioning. +func (srv *ProvisioningSrv) RouteGetAlertRuleGroupExport(c *contextmodel.ReqContext, folder string, group string) response.Response { + g, err := srv.alertRules.GetAlertRuleGroupWithFolderTitle(c.Req.Context(), c.OrgID, folder, group) + if err != nil { + if errors.Is(err, store.ErrAlertRuleGroupNotFound) { + return ErrResp(http.StatusNotFound, err, "") + } + return ErrResp(http.StatusInternalServerError, err, "failed to get alert rule group") + } + + e, err := file.NewAlertingFileExport([]file.AlertRuleGroupWithFolderTitle{g}) + if err != nil { + return ErrResp(http.StatusInternalServerError, err, "failed to create alerting file export") + } + + return exportResponse(c, e) +} + +// RouteGetAlertRuleExport retrieves the given alert rule in a format compatible with file provisioning. +func (srv *ProvisioningSrv) RouteGetAlertRuleExport(c *contextmodel.ReqContext, UID string) response.Response { + rule, err := srv.alertRules.GetAlertRuleWithFolderTitle(c.Req.Context(), c.OrgID, UID) + if err != nil { + if errors.Is(err, alerting_models.ErrAlertRuleNotFound) { + return ErrResp(http.StatusNotFound, err, "") + } + return ErrResp(http.StatusInternalServerError, err, "") + } + + e, err := file.NewAlertingFileExport([]file.AlertRuleGroupWithFolderTitle{{ + AlertRuleGroup: &alerting_models.AlertRuleGroup{ + Title: rule.AlertRule.RuleGroup, + FolderUID: rule.AlertRule.NamespaceUID, + Interval: rule.AlertRule.IntervalSeconds, + Rules: []alerting_models.AlertRule{rule.AlertRule}, + }, + OrgID: c.OrgID, + FolderTitle: rule.FolderTitle, + }}) + if err != nil { + return ErrResp(http.StatusInternalServerError, err, "failed to create alerting file export") + } + + return exportResponse(c, e) +} + +func (srv *ProvisioningSrv) RoutePutAlertRuleGroup(c *contextmodel.ReqContext, ag definitions.AlertRuleGroup, folderUID string, group string) response.Response { ag.FolderUID = folderUID ag.Title = group groupModel, err := ag.ToModel() @@ -354,9 +420,32 @@ func (srv *ProvisioningSrv) RoutePutAlertRuleGroup(c *models.ReqContext, ag defi return response.JSON(http.StatusOK, ag) } -func determineProvenance(ctx *models.ReqContext) alerting_models.Provenance { +func determineProvenance(ctx *contextmodel.ReqContext) alerting_models.Provenance { if _, disabled := ctx.Req.Header[disableProvenanceHeaderName]; disabled { return alerting_models.ProvenanceNone } return alerting_models.ProvenanceAPI } + +func exportResponse(c *contextmodel.ReqContext, body any) response.Response { + format := "json" + acceptHeader := c.Req.Header.Get("Accept") + if strings.Contains(acceptHeader, "yaml") && !strings.Contains(acceptHeader, "json") { + format = "yaml" + } + + download := c.QueryBoolWithDefault("download", false) + if download { + r := response.JSONDownload + if format == "yaml" { + r = response.YAMLDownload + } + return r(http.StatusOK, body, fmt.Sprintf("export.%s", format)) + } + + r := response.JSON + if format == "yaml" { + r = response.YAML + } + return r(http.StatusOK, body) +} diff --git a/pkg/services/ngalert/api/api_provisioning_test.go b/pkg/services/ngalert/api/api_provisioning_test.go index 240751e4c56..2d121d364be 100644 --- a/pkg/services/ngalert/api/api_provisioning_test.go +++ b/pkg/services/ngalert/api/api_provisioning_test.go @@ -5,18 +5,22 @@ import ( "encoding/json" "fmt" "net/http" + "net/http/httptest" + "net/url" "testing" "time" prometheus "github.com/prometheus/alertmanager/config" "github.com/prometheus/alertmanager/timeinterval" "github.com/prometheus/common/model" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" - gfcore "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/provisioning" @@ -370,17 +374,394 @@ func TestProvisioningApi(t *testing.T) { }) }) }) + + t.Run("exports", func(t *testing.T) { + t.Run("alert rule group", func(t *testing.T) { + t.Run("are present, GET returns 200", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule", 1)) + + response := sut.RouteGetAlertRuleGroupExport(&rc, "folder-uid", "my-cool-group") + + require.Equal(t, 200, response.Status()) + }) + + t.Run("are missing, GET returns 404", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule", 1)) + + response := sut.RouteGetAlertRuleGroupExport(&rc, "folder-uid", "does not exist") + + require.Equal(t, 404, response.Status()) + }) + + t.Run("accept header contains yaml, GET returns text yaml", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule", 1)) + + rc.Context.Req.Header.Add("Accept", "application/yaml") + response := sut.RouteGetAlertRuleGroupExport(&rc, "folder-uid", "my-cool-group") + response.WriteTo(&rc) + + require.Equal(t, 200, response.Status()) + require.Equal(t, "text/yaml", rc.Context.Resp.Header().Get("Content-Type")) + }) + + t.Run("accept header contains json, GET returns json", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule", 1)) + + rc.Context.Req.Header.Add("Accept", "application/json") + response := sut.RouteGetAlertRuleGroupExport(&rc, "folder-uid", "my-cool-group") + response.WriteTo(&rc) + + require.Equal(t, 200, response.Status()) + require.Equal(t, "application/json", rc.Context.Resp.Header().Get("Content-Type")) + }) + + t.Run("accept header contains json and yaml, GET returns json", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule", 1)) + + rc.Context.Req.Header.Add("Accept", "application/json, application/yaml") + response := sut.RouteGetAlertRuleGroupExport(&rc, "folder-uid", "my-cool-group") + response.WriteTo(&rc) + + require.Equal(t, 200, response.Status()) + require.Equal(t, "application/json", rc.Context.Resp.Header().Get("Content-Type")) + }) + + t.Run("query param download=true, GET returns content disposition attachment", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule", 1)) + + rc.Context.Req.Form.Set("download", "true") + response := sut.RouteGetAlertRuleGroupExport(&rc, "folder-uid", "my-cool-group") + response.WriteTo(&rc) + + require.Equal(t, 200, response.Status()) + require.Contains(t, rc.Context.Resp.Header().Get("Content-Disposition"), "attachment") + }) + + t.Run("query param download=false, GET returns empty content disposition", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule", 1)) + + rc.Context.Req.Form.Set("download", "false") + response := sut.RouteGetAlertRuleGroupExport(&rc, "folder-uid", "my-cool-group") + response.WriteTo(&rc) + + require.Equal(t, 200, response.Status()) + require.Equal(t, "", rc.Context.Resp.Header().Get("Content-Disposition")) + }) + + t.Run("query param download not set, GET returns empty content disposition", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule", 1)) + + response := sut.RouteGetAlertRuleGroupExport(&rc, "folder-uid", "my-cool-group") + response.WriteTo(&rc) + + require.Equal(t, 200, response.Status()) + require.Equal(t, "", rc.Context.Resp.Header().Get("Content-Disposition")) + }) + + t.Run("json body content is as expected", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule1", 1)) + insertRule(t, sut, createTestAlertRule("rule2", 1)) + + expectedResponse := `{"apiVersion":1,"groups":[{"orgId":1,"name":"my-cool-group","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule1","title":"rule1","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"-100"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false},{"uid":"rule2","title":"rule2","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"-100"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false}]}]}` + + response := sut.RouteGetAlertRuleGroupExport(&rc, "folder-uid", "my-cool-group") + + require.Equal(t, 200, response.Status()) + require.Equal(t, expectedResponse, string(response.Body())) + }) + + t.Run("yaml body content is as expected", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule1", 1)) + insertRule(t, sut, createTestAlertRule("rule2", 1)) + + rc.Context.Req.Header.Add("Accept", "application/yaml") + expectedResponse := "apiVersion: 1\ngroups:\n - orgId: 1\n name: my-cool-group\n folder: Folder Title\n interval: 1m\n rules:\n - uid: rule1\n title: rule1\n condition: A\n data:\n - refId: A\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: \"-100\"\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n isPaused: false\n - uid: rule2\n title: rule2\n condition: A\n data:\n - refId: A\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: \"-100\"\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n isPaused: false\n" + + response := sut.RouteGetAlertRuleGroupExport(&rc, "folder-uid", "my-cool-group") + + require.Equal(t, 200, response.Status()) + require.Equal(t, expectedResponse, string(response.Body())) + }) + }) + + t.Run("alert rule", func(t *testing.T) { + t.Run("are present, GET returns 200", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule1", 1)) + + response := sut.RouteGetAlertRuleExport(&rc, "rule1") + + require.Equal(t, 200, response.Status()) + }) + + t.Run("are missing, GET returns 404", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule1", 1)) + + response := sut.RouteGetAlertRuleExport(&rc, "rule404") + + require.Equal(t, 404, response.Status()) + }) + + t.Run("accept header contains yaml, GET returns text yaml", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule1", 1)) + + rc.Context.Req.Header.Add("Accept", "application/yaml") + response := sut.RouteGetAlertRuleExport(&rc, "rule1") + response.WriteTo(&rc) + + require.Equal(t, 200, response.Status()) + require.Equal(t, "text/yaml", rc.Context.Resp.Header().Get("Content-Type")) + }) + + t.Run("accept header contains json, GET returns json", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule1", 1)) + + rc.Context.Req.Header.Add("Accept", "application/json") + response := sut.RouteGetAlertRuleExport(&rc, "rule1") + response.WriteTo(&rc) + + require.Equal(t, 200, response.Status()) + require.Equal(t, "application/json", rc.Context.Resp.Header().Get("Content-Type")) + }) + + t.Run("accept header contains json and yaml, GET returns json", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule1", 1)) + + rc.Context.Req.Header.Add("Accept", "application/json, application/yaml") + response := sut.RouteGetAlertRuleExport(&rc, "rule1") + response.WriteTo(&rc) + + require.Equal(t, 200, response.Status()) + require.Equal(t, "application/json", rc.Context.Resp.Header().Get("Content-Type")) + }) + + t.Run("query param download=true, GET returns content disposition attachment", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule1", 1)) + + rc.Context.Req.Form.Set("download", "true") + response := sut.RouteGetAlertRuleExport(&rc, "rule1") + response.WriteTo(&rc) + + require.Equal(t, 200, response.Status()) + require.Contains(t, rc.Context.Resp.Header().Get("Content-Disposition"), "attachment") + }) + + t.Run("query param download=false, GET returns empty content disposition", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule1", 1)) + + rc.Context.Req.Form.Set("download", "false") + response := sut.RouteGetAlertRuleExport(&rc, "rule1") + response.WriteTo(&rc) + + require.Equal(t, 200, response.Status()) + require.Equal(t, "", rc.Context.Resp.Header().Get("Content-Disposition")) + }) + + t.Run("query param download not set, GET returns empty content disposition", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule1", 1)) + + response := sut.RouteGetAlertRuleExport(&rc, "rule1") + response.WriteTo(&rc) + + require.Equal(t, 200, response.Status()) + require.Equal(t, "", rc.Context.Resp.Header().Get("Content-Disposition")) + }) + + t.Run("json body content is as expected", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule1", 1)) + + expectedResponse := `{"apiVersion":1,"groups":[{"orgId":1,"name":"my-cool-group","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule1","title":"rule1","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"-100"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false}]}]}` + + response := sut.RouteGetAlertRuleExport(&rc, "rule1") + + require.Equal(t, 200, response.Status()) + require.Equal(t, expectedResponse, string(response.Body())) + }) + + t.Run("yaml body content is as expected", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule1", 1)) + + rc.Context.Req.Header.Add("Accept", "application/yaml") + expectedResponse := "apiVersion: 1\ngroups:\n - orgId: 1\n name: my-cool-group\n folder: Folder Title\n interval: 1m\n rules:\n - uid: rule1\n title: rule1\n condition: A\n data:\n - refId: A\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: \"-100\"\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n isPaused: false\n" + + response := sut.RouteGetAlertRuleExport(&rc, "rule1") + + require.Equal(t, 200, response.Status()) + require.Equal(t, expectedResponse, string(response.Body())) + }) + }) + + t.Run("all alert rules", func(t *testing.T) { + t.Run("are present, GET returns 200", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule", 1)) + + response := sut.RouteGetAlertRulesExport(&rc) + + require.Equal(t, 200, response.Status()) + }) + + t.Run("accept header contains yaml, GET returns text yaml", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule", 1)) + + rc.Context.Req.Header.Add("Accept", "application/yaml") + response := sut.RouteGetAlertRulesExport(&rc) + response.WriteTo(&rc) + + require.Equal(t, 200, response.Status()) + require.Equal(t, "text/yaml", rc.Context.Resp.Header().Get("Content-Type")) + }) + + t.Run("accept header contains json, GET returns json", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule", 1)) + + rc.Context.Req.Header.Add("Accept", "application/json") + response := sut.RouteGetAlertRulesExport(&rc) + response.WriteTo(&rc) + + require.Equal(t, 200, response.Status()) + require.Equal(t, "application/json", rc.Context.Resp.Header().Get("Content-Type")) + }) + + t.Run("accept header contains json and yaml, GET returns json", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule", 1)) + + rc.Context.Req.Header.Add("Accept", "application/json, application/yaml") + response := sut.RouteGetAlertRulesExport(&rc) + response.WriteTo(&rc) + + require.Equal(t, 200, response.Status()) + require.Equal(t, "application/json", rc.Context.Resp.Header().Get("Content-Type")) + }) + + t.Run("query param download=true, GET returns content disposition attachment", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule", 1)) + + rc.Context.Req.Form.Set("download", "true") + response := sut.RouteGetAlertRulesExport(&rc) + response.WriteTo(&rc) + + require.Equal(t, 200, response.Status()) + require.Contains(t, rc.Context.Resp.Header().Get("Content-Disposition"), "attachment") + }) + + t.Run("query param download=false, GET returns empty content disposition", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule", 1)) + + rc.Context.Req.Form.Set("download", "false") + response := sut.RouteGetAlertRulesExport(&rc) + response.WriteTo(&rc) + + require.Equal(t, 200, response.Status()) + require.Equal(t, "", rc.Context.Resp.Header().Get("Content-Disposition")) + }) + + t.Run("query param download not set, GET returns empty content disposition", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRule("rule", 1)) + + response := sut.RouteGetAlertRulesExport(&rc) + response.WriteTo(&rc) + + require.Equal(t, 200, response.Status()) + require.Equal(t, "", rc.Context.Resp.Header().Get("Content-Disposition")) + }) + + t.Run("json body content is as expected", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRuleWithFolderAndGroup("rule1", 1, "folder-uid", "groupa")) + insertRule(t, sut, createTestAlertRuleWithFolderAndGroup("rule2", 1, "folder-uid", "groupb")) + insertRule(t, sut, createTestAlertRuleWithFolderAndGroup("rule3", 1, "folder-uid2", "groupb")) + + expectedResponse := `{"apiVersion":1,"groups":[{"orgId":1,"name":"groupa","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule1","title":"rule1","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"-100"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false}]},{"orgId":1,"name":"groupb","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule2","title":"rule2","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"-100"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false}]},{"orgId":1,"name":"groupb","folder":"Folder Title2","interval":"1m","rules":[{"uid":"rule3","title":"rule3","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"-100"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false}]}]}` + + response := sut.RouteGetAlertRulesExport(&rc) + + require.Equal(t, 200, response.Status()) + require.Equal(t, expectedResponse, string(response.Body())) + }) + + t.Run("yaml body content is as expected", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + insertRule(t, sut, createTestAlertRuleWithFolderAndGroup("rule1", 1, "folder-uid", "groupa")) + insertRule(t, sut, createTestAlertRuleWithFolderAndGroup("rule2", 1, "folder-uid", "groupb")) + insertRule(t, sut, createTestAlertRuleWithFolderAndGroup("rule3", 1, "folder-uid2", "groupb")) + + rc.Context.Req.Header.Add("Accept", "application/yaml") + expectedResponse := "apiVersion: 1\ngroups:\n - orgId: 1\n name: groupa\n folder: Folder Title\n interval: 1m\n rules:\n - uid: rule1\n title: rule1\n condition: A\n data:\n - refId: A\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: \"-100\"\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n isPaused: false\n - orgId: 1\n name: groupb\n folder: Folder Title\n interval: 1m\n rules:\n - uid: rule2\n title: rule2\n condition: A\n data:\n - refId: A\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: \"-100\"\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n isPaused: false\n - orgId: 1\n name: groupb\n folder: Folder Title2\n interval: 1m\n rules:\n - uid: rule3\n title: rule3\n condition: A\n data:\n - refId: A\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: \"-100\"\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n isPaused: false\n" + + response := sut.RouteGetAlertRulesExport(&rc) + + require.Equal(t, 200, response.Status()) + require.Equal(t, expectedResponse, string(response.Body())) + }) + }) + }) } // testEnvironment binds together common dependencies for testing alerting APIs. type testEnvironment struct { - secrets secrets.Service - log log.Logger - store store.DBstore - configs provisioning.AMConfigStore - xact provisioning.TransactionManager - quotas provisioning.QuotaChecker - prov provisioning.ProvisioningStore + secrets secrets.Service + log log.Logger + store store.DBstore + dashboardService dashboards.DashboardService + configs provisioning.AMConfigStore + xact provisioning.TransactionManager + quotas provisioning.QuotaChecker + prov provisioning.ProvisioningStore } func createTestEnv(t *testing.T) testEnvironment { @@ -407,14 +788,29 @@ func createTestEnv(t *testing.T) testEnvironment { prov.EXPECT().SaveSucceeds() prov.EXPECT().GetReturns(models.ProvenanceNone) + dashboardService := dashboards.NewFakeDashboardService(t) + dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Return(&dashboards.Dashboard{ + UID: "folder-uid", + Title: "Folder Title", + }, nil).Maybe() + dashboardService.On("GetDashboards", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardsQuery")).Return([]*dashboards.Dashboard{{ + UID: "folder-uid", + Title: "Folder Title", + }, + { + UID: "folder-uid2", + Title: "Folder Title2", + }}, nil).Maybe() + return testEnvironment{ - secrets: secrets, - log: log, - configs: configs, - store: store, - xact: xact, - prov: prov, - quotas: quotas, + secrets: secrets, + log: log, + configs: configs, + store: store, + dashboardService: dashboardService, + xact: xact, + prov: prov, + quotas: quotas, } } @@ -434,14 +830,18 @@ func createProvisioningSrvSutFromEnv(t *testing.T, env *testEnvironment) Provisi contactPointService: provisioning.NewContactPointService(env.configs, env.secrets, env.prov, env.xact, env.log), templates: provisioning.NewTemplateService(env.configs, env.prov, env.xact, env.log), muteTimings: provisioning.NewMuteTimingService(env.configs, env.prov, env.xact, env.log), - alertRules: provisioning.NewAlertRuleService(env.store, env.prov, env.quotas, env.xact, 60, 10, env.log), + alertRules: provisioning.NewAlertRuleService(env.store, env.prov, env.dashboardService, env.quotas, env.xact, 60, 10, env.log), } } -func createTestRequestCtx() gfcore.ReqContext { - return gfcore.ReqContext{ +func createTestRequestCtx() contextmodel.ReqContext { + return contextmodel.ReqContext{ Context: &web.Context{ - Req: &http.Request{}, + Req: &http.Request{ + Header: make(http.Header), + Form: make(url.Values), + }, + Resp: web.NewResponseWriter("GET", httptest.NewRecorder()), }, SignedInUser: &user.SignedInUser{ OrgID: 1, @@ -555,15 +955,23 @@ func createInvalidAlertRuleGroup() definitions.AlertRuleGroup { } } +func createTestAlertRuleWithFolderAndGroup(title string, orgID int64, folderUid string, group string) definitions.ProvisionedAlertRule { + rule := createTestAlertRule(title, orgID) + rule.FolderUID = folderUid + rule.RuleGroup = group + return rule +} + func createTestAlertRule(title string, orgID int64) definitions.ProvisionedAlertRule { return definitions.ProvisionedAlertRule{ + UID: title, OrgID: orgID, Title: title, Condition: "A", Data: []models.AlertQuery{ { RefID: "A", - Model: json.RawMessage("{}"), + Model: json.RawMessage(testModel), RelativeTimeRange: models.RelativeTimeRange{ From: models.Duration(60), To: models.Duration(0), @@ -600,6 +1008,42 @@ func deserializeRule(t *testing.T, data []byte) definitions.ProvisionedAlertRule return rule } +var testModel = ` +{ + "conditions": [ + { + "evaluator": { + "params": [ + 3 + ], + "type": "gt" + }, + "operator": { + "type": "and" + }, + "query": { + "params": [ + "A" + ] + }, + "reducer": { + "type": "last" + }, + "type": "query" + } + ], + "datasource": { + "type": "__expr__", + "uid": "-100" + }, + "expression": "1==0", + "intervalMs": 1000, + "maxDataPoints": 43200, + "refId": "A", + "type": "math" +} +` + var testConfig = ` { "template_files": { diff --git a/pkg/services/ngalert/api/api_ruler.go b/pkg/services/ngalert/api/api_ruler.go index 635899411b2..b467a3b4034 100644 --- a/pkg/services/ngalert/api/api_ruler.go +++ b/pkg/services/ngalert/api/api_ruler.go @@ -8,23 +8,22 @@ import ( "strings" "time" - "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/dashboards" - "github.com/grafana/grafana/pkg/services/ngalert/eval" - "github.com/grafana/grafana/pkg/services/ngalert/provisioning" - "github.com/grafana/grafana/pkg/services/ngalert/store" - "github.com/grafana/grafana/pkg/services/quota" - "github.com/grafana/grafana/pkg/setting" - "github.com/prometheus/common/model" "github.com/grafana/grafana/pkg/api/apierrors" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" + "github.com/grafana/grafana/pkg/services/dashboards" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + "github.com/grafana/grafana/pkg/services/ngalert/eval" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/services/ngalert/provisioning" "github.com/grafana/grafana/pkg/services/ngalert/schedule" + "github.com/grafana/grafana/pkg/services/ngalert/store" + "github.com/grafana/grafana/pkg/services/quota" + "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) @@ -53,7 +52,7 @@ var ( // or, if non-empty, a specific group of rules in the namespace. // Returns http.StatusUnauthorized if user does not have access to any of the rules that match the filter. // Returns http.StatusBadRequest if all rules that match the filter and the user is authorized to delete are provisioned. -func (srv RulerSrv) RouteDeleteAlertRules(c *models.ReqContext, namespaceTitle string, group string) response.Response { +func (srv RulerSrv) RouteDeleteAlertRules(c *contextmodel.ReqContext, namespaceTitle string, group string) response.Response { namespace, err := srv.store.GetNamespaceByTitle(c.Req.Context(), namespaceTitle, c.SignedInUser.OrgID, c.SignedInUser, true) if err != nil { return toNamespaceErrorResponse(err) @@ -155,7 +154,7 @@ func (srv RulerSrv) RouteDeleteAlertRules(c *models.ReqContext, namespaceTitle s } // RouteGetNamespaceRulesConfig returns all rules in a specific folder that user has access to -func (srv RulerSrv) RouteGetNamespaceRulesConfig(c *models.ReqContext, namespaceTitle string) response.Response { +func (srv RulerSrv) RouteGetNamespaceRulesConfig(c *contextmodel.ReqContext, namespaceTitle string) response.Response { namespace, err := srv.store.GetNamespaceByTitle(c.Req.Context(), namespaceTitle, c.SignedInUser.OrgID, c.SignedInUser, false) if err != nil { return toNamespaceErrorResponse(err) @@ -197,7 +196,7 @@ func (srv RulerSrv) RouteGetNamespaceRulesConfig(c *models.ReqContext, namespace // RouteGetRulesGroupConfig returns rules that belong to a specific group in a specific namespace (folder). // If user does not have access to at least one of the rule in the group, returns status 401 Unauthorized -func (srv RulerSrv) RouteGetRulesGroupConfig(c *models.ReqContext, namespaceTitle string, ruleGroup string) response.Response { +func (srv RulerSrv) RouteGetRulesGroupConfig(c *contextmodel.ReqContext, namespaceTitle string, ruleGroup string) response.Response { namespace, err := srv.store.GetNamespaceByTitle(c.Req.Context(), namespaceTitle, c.SignedInUser.OrgID, c.SignedInUser, false) if err != nil { return toNamespaceErrorResponse(err) @@ -232,7 +231,7 @@ func (srv RulerSrv) RouteGetRulesGroupConfig(c *models.ReqContext, namespaceTitl } // RouteGetRulesConfig returns all alert rules that are available to the current user -func (srv RulerSrv) RouteGetRulesConfig(c *models.ReqContext) response.Response { +func (srv RulerSrv) RouteGetRulesConfig(c *contextmodel.ReqContext) response.Response { namespaceMap, err := srv.store.GetUserVisibleNamespaces(c.Req.Context(), c.OrgID, c.SignedInUser) if err != nil { return ErrResp(http.StatusInternalServerError, err, "failed to get namespaces visible to the user") @@ -301,7 +300,7 @@ func (srv RulerSrv) RouteGetRulesConfig(c *models.ReqContext) response.Response return response.JSON(http.StatusOK, result) } -func (srv RulerSrv) RoutePostNameRulesConfig(c *models.ReqContext, ruleGroupConfig apimodels.PostableRuleGroupConfig, namespaceTitle string) response.Response { +func (srv RulerSrv) RoutePostNameRulesConfig(c *contextmodel.ReqContext, ruleGroupConfig apimodels.PostableRuleGroupConfig, namespaceTitle string) response.Response { namespace, err := srv.store.GetNamespaceByTitle(c.Req.Context(), namespaceTitle, c.SignedInUser.OrgID, c.SignedInUser, true) if err != nil { return toNamespaceErrorResponse(err) @@ -325,7 +324,7 @@ func (srv RulerSrv) RoutePostNameRulesConfig(c *models.ReqContext, ruleGroupConf // updateAlertRulesInGroup calculates changes (rules to add,update,delete), verifies that the user is authorized to do the calculated changes and updates database. // All operations are performed in a single transaction -func (srv RulerSrv) updateAlertRulesInGroup(c *models.ReqContext, groupKey ngmodels.AlertRuleGroupKey, rules []*ngmodels.AlertRule) response.Response { +func (srv RulerSrv) updateAlertRulesInGroup(c *contextmodel.ReqContext, groupKey ngmodels.AlertRuleGroupKey, rules []*ngmodels.AlertRule) response.Response { var finalChanges *store.GroupDelta hasAccess := accesscontrol.HasAccess(srv.ac, c) err := srv.xactManager.InTransaction(c.Req.Context(), func(tranCtx context.Context) error { diff --git a/pkg/services/ngalert/api/api_ruler_test.go b/pkg/services/ngalert/api/api_ruler_test.go index 9f7d07103a2..43ee46caa8e 100644 --- a/pkg/services/ngalert/api/api_ruler_test.go +++ b/pkg/services/ngalert/api/api_ruler_test.go @@ -14,9 +14,9 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/log" - models2 "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" acMock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/folder" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" @@ -657,7 +657,7 @@ func createService(ac *acMock.Mock, store *fakes.RuleStore, scheduler schedule.S } } -func createRequestContext(orgID int64, role org.RoleType, params map[string]string) *models2.ReqContext { +func createRequestContext(orgID int64, role org.RoleType, params map[string]string) *contextmodel.ReqContext { uri, _ := url.Parse("http://localhost") ctx := web.Context{Req: &http.Request{ URL: uri, @@ -666,7 +666,7 @@ func createRequestContext(orgID int64, role org.RoleType, params map[string]stri ctx.Req = web.SetURLParams(ctx.Req, params) } - return &models2.ReqContext{ + return &contextmodel.ReqContext{ IsSignedIn: true, SignedInUser: &user.SignedInUser{ OrgRole: role, diff --git a/pkg/services/ngalert/api/api_testing.go b/pkg/services/ngalert/api/api_testing.go index 91a3d09a1dd..acc9e0d6614 100644 --- a/pkg/services/ngalert/api/api_testing.go +++ b/pkg/services/ngalert/api/api_testing.go @@ -12,8 +12,8 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/featuremgmt" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" @@ -35,7 +35,7 @@ type TestingApiSrv struct { featureManager featuremgmt.FeatureToggles } -func (srv TestingApiSrv) RouteTestGrafanaRuleConfig(c *models.ReqContext, body apimodels.TestRulePayload) response.Response { +func (srv TestingApiSrv) RouteTestGrafanaRuleConfig(c *contextmodel.ReqContext, body apimodels.TestRulePayload) response.Response { if body.Type() != apimodels.GrafanaBackend || body.GrafanaManagedCondition == nil { return errorToResponse(backendTypeDoesNotMatchPayloadTypeError(apimodels.GrafanaBackend, body.Type().String())) } @@ -73,7 +73,7 @@ func (srv TestingApiSrv) RouteTestGrafanaRuleConfig(c *models.ReqContext, body a }) } -func (srv TestingApiSrv) RouteTestRuleConfig(c *models.ReqContext, body apimodels.TestRulePayload, datasourceUID string) response.Response { +func (srv TestingApiSrv) RouteTestRuleConfig(c *contextmodel.ReqContext, body apimodels.TestRulePayload, datasourceUID string) response.Response { if body.Type() != apimodels.LoTexRulerBackend { return errorToResponse(backendTypeDoesNotMatchPayloadTypeError(apimodels.LoTexRulerBackend, body.Type().String())) } @@ -111,7 +111,7 @@ func (srv TestingApiSrv) RouteTestRuleConfig(c *models.ReqContext, body apimodel ) } -func (srv TestingApiSrv) RouteEvalQueries(c *models.ReqContext, cmd apimodels.EvalQueriesPayload) response.Response { +func (srv TestingApiSrv) RouteEvalQueries(c *contextmodel.ReqContext, cmd apimodels.EvalQueriesPayload) response.Response { if !authorizeDatasourceAccessForRule(&ngmodels.AlertRule{Data: cmd.Data}, func(evaluator accesscontrol.Evaluator) bool { return accesscontrol.HasAccess(srv.accessControl, c)(accesscontrol.ReqSignedIn, evaluator) }) { @@ -145,7 +145,7 @@ func (srv TestingApiSrv) RouteEvalQueries(c *models.ReqContext, cmd apimodels.Ev return response.JSONStreaming(http.StatusOK, evalResults) } -func (srv TestingApiSrv) BacktestAlertRule(c *models.ReqContext, cmd apimodels.BacktestConfig) response.Response { +func (srv TestingApiSrv) BacktestAlertRule(c *contextmodel.ReqContext, cmd apimodels.BacktestConfig) response.Response { if !srv.featureManager.IsEnabled(featuremgmt.FlagAlertingBacktesting) { return ErrResp(http.StatusNotFound, nil, "Backgtesting API is not enabled") } diff --git a/pkg/services/ngalert/api/api_testing_test.go b/pkg/services/ngalert/api/api_testing_test.go index e76f5ca7749..af0d5d03047 100644 --- a/pkg/services/ngalert/api/api_testing_test.go +++ b/pkg/services/ngalert/api/api_testing_test.go @@ -9,9 +9,9 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - models2 "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" acMock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" fakes "github.com/grafana/grafana/pkg/services/datasources/fakes" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" @@ -24,7 +24,7 @@ import ( func TestRouteTestGrafanaRuleConfig(t *testing.T) { t.Run("when fine-grained access is enabled", func(t *testing.T) { - rc := &models2.ReqContext{ + rc := &contextmodel.ReqContext{ Context: &web.Context{ Req: &http.Request{}, }, @@ -95,7 +95,7 @@ func TestRouteTestGrafanaRuleConfig(t *testing.T) { }) t.Run("when fine-grained access is disabled", func(t *testing.T) { - rc := &models2.ReqContext{ + rc := &contextmodel.ReqContext{ Context: &web.Context{ Req: &http.Request{}, }, @@ -152,7 +152,7 @@ func TestRouteTestGrafanaRuleConfig(t *testing.T) { func TestRouteEvalQueries(t *testing.T) { t.Run("when fine-grained access is enabled", func(t *testing.T) { - rc := &models2.ReqContext{ + rc := &contextmodel.ReqContext{ Context: &web.Context{ Req: &http.Request{}, }, @@ -222,7 +222,7 @@ func TestRouteEvalQueries(t *testing.T) { }) t.Run("when fine-grained access is disabled", func(t *testing.T) { - rc := &models2.ReqContext{ + rc := &contextmodel.ReqContext{ Context: &web.Context{ Req: &http.Request{}, }, diff --git a/pkg/services/ngalert/api/authorization.go b/pkg/services/ngalert/api/authorization.go index 4dfc8a85ab3..0a6c2a4d005 100644 --- a/pkg/services/ngalert/api/authorization.go +++ b/pkg/services/ngalert/api/authorization.go @@ -201,7 +201,10 @@ func (api *API) authorize(method, path string) web.Handler { http.MethodGet + "/api/v1/provisioning/mute-timings/{name}", http.MethodGet + "/api/v1/provisioning/alert-rules", http.MethodGet + "/api/v1/provisioning/alert-rules/{UID}", - http.MethodGet + "/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}": + http.MethodGet + "/api/v1/provisioning/alert-rules/export", + http.MethodGet + "/api/v1/provisioning/alert-rules/{UID}/export", + http.MethodGet + "/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}", + http.MethodGet + "/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}/export": fallback = middleware.ReqOrgAdmin eval = ac.EvalPermission(ac.ActionAlertingProvisioningRead) // organization scope diff --git a/pkg/services/ngalert/api/authorization_test.go b/pkg/services/ngalert/api/authorization_test.go index b4da61566fd..2957979a26d 100644 --- a/pkg/services/ngalert/api/authorization_test.go +++ b/pkg/services/ngalert/api/authorization_test.go @@ -49,7 +49,7 @@ func TestAuthorize(t *testing.T) { } paths[p] = methods } - require.Len(t, paths, 41) + require.Len(t, paths, 44) ac := acmock.New() api := &API{AccessControl: ac} diff --git a/pkg/services/ngalert/api/configuration.go b/pkg/services/ngalert/api/configuration.go index 7bc382c92ab..4998f81a960 100644 --- a/pkg/services/ngalert/api/configuration.go +++ b/pkg/services/ngalert/api/configuration.go @@ -2,7 +2,7 @@ package api import ( "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" ) @@ -17,22 +17,22 @@ func NewConfiguration(grafana *ConfigSrv) *ConfigurationApiHandler { } } -func (f *ConfigurationApiHandler) handleRouteGetAlertmanagers(c *models.ReqContext) response.Response { +func (f *ConfigurationApiHandler) handleRouteGetAlertmanagers(c *contextmodel.ReqContext) response.Response { return f.grafana.RouteGetAlertmanagers(c) } -func (f *ConfigurationApiHandler) handleRouteGetNGalertConfig(c *models.ReqContext) response.Response { +func (f *ConfigurationApiHandler) handleRouteGetNGalertConfig(c *contextmodel.ReqContext) response.Response { return f.grafana.RouteGetNGalertConfig(c) } -func (f *ConfigurationApiHandler) handleRoutePostNGalertConfig(c *models.ReqContext, body apimodels.PostableNGalertConfig) response.Response { +func (f *ConfigurationApiHandler) handleRoutePostNGalertConfig(c *contextmodel.ReqContext, body apimodels.PostableNGalertConfig) response.Response { return f.grafana.RoutePostNGalertConfig(c, body) } -func (f *ConfigurationApiHandler) handleRouteDeleteNGalertConfig(c *models.ReqContext) response.Response { +func (f *ConfigurationApiHandler) handleRouteDeleteNGalertConfig(c *contextmodel.ReqContext) response.Response { return f.grafana.RouteDeleteNGalertConfig(c) } -func (f *ConfigurationApiHandler) handleRouteGetStatus(c *models.ReqContext) response.Response { +func (f *ConfigurationApiHandler) handleRouteGetStatus(c *contextmodel.ReqContext) response.Response { return f.grafana.RouteGetAlertingStatus(c) } diff --git a/pkg/services/ngalert/api/forking_alertmanager.go b/pkg/services/ngalert/api/forking_alertmanager.go index c6d2a43ef8b..a45f35c9a98 100644 --- a/pkg/services/ngalert/api/forking_alertmanager.go +++ b/pkg/services/ngalert/api/forking_alertmanager.go @@ -2,7 +2,7 @@ package api import ( "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" ) @@ -22,7 +22,7 @@ func NewForkingAM(datasourceCache datasources.CacheService, proxy *LotexAM, graf } } -func (f *AlertmanagerApiHandler) getService(ctx *models.ReqContext) (*LotexAM, error) { +func (f *AlertmanagerApiHandler) getService(ctx *contextmodel.ReqContext) (*LotexAM, error) { _, err := getDatasourceByUID(ctx, f.DatasourceCache, apimodels.AlertmanagerBackend) if err != nil { return nil, err @@ -30,7 +30,7 @@ func (f *AlertmanagerApiHandler) getService(ctx *models.ReqContext) (*LotexAM, e return f.AMSvc, nil } -func (f *AlertmanagerApiHandler) handleRouteGetAMStatus(ctx *models.ReqContext, dsUID string) response.Response { +func (f *AlertmanagerApiHandler) handleRouteGetAMStatus(ctx *contextmodel.ReqContext, dsUID string) response.Response { s, err := f.getService(ctx) if err != nil { return errorToResponse(err) @@ -39,7 +39,7 @@ func (f *AlertmanagerApiHandler) handleRouteGetAMStatus(ctx *models.ReqContext, return s.RouteGetAMStatus(ctx) } -func (f *AlertmanagerApiHandler) handleRouteCreateSilence(ctx *models.ReqContext, body apimodels.PostableSilence, dsUID string) response.Response { +func (f *AlertmanagerApiHandler) handleRouteCreateSilence(ctx *contextmodel.ReqContext, body apimodels.PostableSilence, dsUID string) response.Response { s, err := f.getService(ctx) if err != nil { return errorToResponse(err) @@ -48,7 +48,7 @@ func (f *AlertmanagerApiHandler) handleRouteCreateSilence(ctx *models.ReqContext return s.RouteCreateSilence(ctx, body) } -func (f *AlertmanagerApiHandler) handleRouteDeleteAlertingConfig(ctx *models.ReqContext, dsUID string) response.Response { +func (f *AlertmanagerApiHandler) handleRouteDeleteAlertingConfig(ctx *contextmodel.ReqContext, dsUID string) response.Response { s, err := f.getService(ctx) if err != nil { return errorToResponse(err) @@ -57,7 +57,7 @@ func (f *AlertmanagerApiHandler) handleRouteDeleteAlertingConfig(ctx *models.Req return s.RouteDeleteAlertingConfig(ctx) } -func (f *AlertmanagerApiHandler) handleRouteDeleteSilence(ctx *models.ReqContext, silenceID string, dsUID string) response.Response { +func (f *AlertmanagerApiHandler) handleRouteDeleteSilence(ctx *contextmodel.ReqContext, silenceID string, dsUID string) response.Response { s, err := f.getService(ctx) if err != nil { return errorToResponse(err) @@ -66,7 +66,7 @@ func (f *AlertmanagerApiHandler) handleRouteDeleteSilence(ctx *models.ReqContext return s.RouteDeleteSilence(ctx, silenceID) } -func (f *AlertmanagerApiHandler) handleRouteGetAlertingConfig(ctx *models.ReqContext, dsUID string) response.Response { +func (f *AlertmanagerApiHandler) handleRouteGetAlertingConfig(ctx *contextmodel.ReqContext, dsUID string) response.Response { s, err := f.getService(ctx) if err != nil { return errorToResponse(err) @@ -75,7 +75,7 @@ func (f *AlertmanagerApiHandler) handleRouteGetAlertingConfig(ctx *models.ReqCon return s.RouteGetAlertingConfig(ctx) } -func (f *AlertmanagerApiHandler) handleRouteGetAMAlertGroups(ctx *models.ReqContext, dsUID string) response.Response { +func (f *AlertmanagerApiHandler) handleRouteGetAMAlertGroups(ctx *contextmodel.ReqContext, dsUID string) response.Response { s, err := f.getService(ctx) if err != nil { return errorToResponse(err) @@ -84,7 +84,7 @@ func (f *AlertmanagerApiHandler) handleRouteGetAMAlertGroups(ctx *models.ReqCont return s.RouteGetAMAlertGroups(ctx) } -func (f *AlertmanagerApiHandler) handleRouteGetAMAlerts(ctx *models.ReqContext, dsUID string) response.Response { +func (f *AlertmanagerApiHandler) handleRouteGetAMAlerts(ctx *contextmodel.ReqContext, dsUID string) response.Response { s, err := f.getService(ctx) if err != nil { return errorToResponse(err) @@ -93,7 +93,7 @@ func (f *AlertmanagerApiHandler) handleRouteGetAMAlerts(ctx *models.ReqContext, return s.RouteGetAMAlerts(ctx) } -func (f *AlertmanagerApiHandler) handleRouteGetSilence(ctx *models.ReqContext, silenceID string, dsUID string) response.Response { +func (f *AlertmanagerApiHandler) handleRouteGetSilence(ctx *contextmodel.ReqContext, silenceID string, dsUID string) response.Response { s, err := f.getService(ctx) if err != nil { return errorToResponse(err) @@ -102,7 +102,7 @@ func (f *AlertmanagerApiHandler) handleRouteGetSilence(ctx *models.ReqContext, s return s.RouteGetSilence(ctx, silenceID) } -func (f *AlertmanagerApiHandler) handleRouteGetSilences(ctx *models.ReqContext, dsUID string) response.Response { +func (f *AlertmanagerApiHandler) handleRouteGetSilences(ctx *contextmodel.ReqContext, dsUID string) response.Response { s, err := f.getService(ctx) if err != nil { return errorToResponse(err) @@ -111,7 +111,7 @@ func (f *AlertmanagerApiHandler) handleRouteGetSilences(ctx *models.ReqContext, return s.RouteGetSilences(ctx) } -func (f *AlertmanagerApiHandler) handleRoutePostAlertingConfig(ctx *models.ReqContext, body apimodels.PostableUserConfig, dsUID string) response.Response { +func (f *AlertmanagerApiHandler) handleRoutePostAlertingConfig(ctx *contextmodel.ReqContext, body apimodels.PostableUserConfig, dsUID string) response.Response { s, err := f.getService(ctx) if err != nil { return errorToResponse(err) @@ -122,7 +122,7 @@ func (f *AlertmanagerApiHandler) handleRoutePostAlertingConfig(ctx *models.ReqCo return s.RoutePostAlertingConfig(ctx, body) } -func (f *AlertmanagerApiHandler) handleRoutePostAMAlerts(ctx *models.ReqContext, body apimodels.PostableAlerts, dsUID string) response.Response { +func (f *AlertmanagerApiHandler) handleRoutePostAMAlerts(ctx *contextmodel.ReqContext, body apimodels.PostableAlerts, dsUID string) response.Response { s, err := f.getService(ctx) if err != nil { return errorToResponse(err) @@ -131,53 +131,53 @@ func (f *AlertmanagerApiHandler) handleRoutePostAMAlerts(ctx *models.ReqContext, return s.RoutePostAMAlerts(ctx, body) } -func (f *AlertmanagerApiHandler) handleRouteDeleteGrafanaSilence(ctx *models.ReqContext, id string) response.Response { +func (f *AlertmanagerApiHandler) handleRouteDeleteGrafanaSilence(ctx *contextmodel.ReqContext, id string) response.Response { return f.GrafanaSvc.RouteDeleteSilence(ctx, id) } -func (f *AlertmanagerApiHandler) handleRouteDeleteGrafanaAlertingConfig(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) handleRouteDeleteGrafanaAlertingConfig(ctx *contextmodel.ReqContext) response.Response { return f.GrafanaSvc.RouteDeleteAlertingConfig(ctx) } -func (f *AlertmanagerApiHandler) handleRouteCreateGrafanaSilence(ctx *models.ReqContext, body apimodels.PostableSilence) response.Response { +func (f *AlertmanagerApiHandler) handleRouteCreateGrafanaSilence(ctx *contextmodel.ReqContext, body apimodels.PostableSilence) response.Response { return f.GrafanaSvc.RouteCreateSilence(ctx, body) } -func (f *AlertmanagerApiHandler) handleRouteGetGrafanaAMStatus(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) handleRouteGetGrafanaAMStatus(ctx *contextmodel.ReqContext) response.Response { return f.GrafanaSvc.RouteGetAMStatus(ctx) } -func (f *AlertmanagerApiHandler) handleRouteGetGrafanaAMAlerts(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) handleRouteGetGrafanaAMAlerts(ctx *contextmodel.ReqContext) response.Response { return f.GrafanaSvc.RouteGetAMAlerts(ctx) } -func (f *AlertmanagerApiHandler) handleRouteGetGrafanaAMAlertGroups(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) handleRouteGetGrafanaAMAlertGroups(ctx *contextmodel.ReqContext) response.Response { return f.GrafanaSvc.RouteGetAMAlertGroups(ctx) } -func (f *AlertmanagerApiHandler) handleRouteGetGrafanaAlertingConfig(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) handleRouteGetGrafanaAlertingConfig(ctx *contextmodel.ReqContext) response.Response { return f.GrafanaSvc.RouteGetAlertingConfig(ctx) } -func (f *AlertmanagerApiHandler) handleRouteGetGrafanaSilence(ctx *models.ReqContext, id string) response.Response { +func (f *AlertmanagerApiHandler) handleRouteGetGrafanaSilence(ctx *contextmodel.ReqContext, id string) response.Response { return f.GrafanaSvc.RouteGetSilence(ctx, id) } -func (f *AlertmanagerApiHandler) handleRouteGetGrafanaSilences(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) handleRouteGetGrafanaSilences(ctx *contextmodel.ReqContext) response.Response { return f.GrafanaSvc.RouteGetSilences(ctx) } -func (f *AlertmanagerApiHandler) handleRoutePostGrafanaAlertingConfig(ctx *models.ReqContext, conf apimodels.PostableUserConfig) response.Response { +func (f *AlertmanagerApiHandler) handleRoutePostGrafanaAlertingConfig(ctx *contextmodel.ReqContext, conf apimodels.PostableUserConfig) response.Response { if !conf.AlertmanagerConfig.ReceiverType().Can(apimodels.GrafanaReceiverType) { return errorToResponse(backendTypeDoesNotMatchPayloadTypeError(apimodels.GrafanaBackend, conf.AlertmanagerConfig.ReceiverType().String())) } return f.GrafanaSvc.RoutePostAlertingConfig(ctx, conf) } -func (f *AlertmanagerApiHandler) handleRouteGetGrafanaReceivers(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) handleRouteGetGrafanaReceivers(ctx *contextmodel.ReqContext) response.Response { return f.GrafanaSvc.RouteGetReceivers(ctx) } -func (f *AlertmanagerApiHandler) handleRoutePostTestGrafanaReceivers(ctx *models.ReqContext, conf apimodels.TestReceiversConfigBodyParams) response.Response { +func (f *AlertmanagerApiHandler) handleRoutePostTestGrafanaReceivers(ctx *contextmodel.ReqContext, conf apimodels.TestReceiversConfigBodyParams) response.Response { return f.GrafanaSvc.RoutePostTestReceivers(ctx, conf) } diff --git a/pkg/services/ngalert/api/forking_prometheus.go b/pkg/services/ngalert/api/forking_prometheus.go index 492983e6a7e..c0fa78b9f74 100644 --- a/pkg/services/ngalert/api/forking_prometheus.go +++ b/pkg/services/ngalert/api/forking_prometheus.go @@ -2,7 +2,7 @@ package api import ( "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" ) @@ -22,7 +22,7 @@ func NewForkingProm(datasourceCache datasources.CacheService, proxy *LotexProm, } } -func (f *PrometheusApiHandler) handleRouteGetAlertStatuses(ctx *models.ReqContext, dsUID string) response.Response { +func (f *PrometheusApiHandler) handleRouteGetAlertStatuses(ctx *contextmodel.ReqContext, dsUID string) response.Response { t, err := f.getService(ctx) if err != nil { return errorToResponse(err) @@ -30,7 +30,7 @@ func (f *PrometheusApiHandler) handleRouteGetAlertStatuses(ctx *models.ReqContex return t.RouteGetAlertStatuses(ctx) } -func (f *PrometheusApiHandler) handleRouteGetRuleStatuses(ctx *models.ReqContext, dsUID string) response.Response { +func (f *PrometheusApiHandler) handleRouteGetRuleStatuses(ctx *contextmodel.ReqContext, dsUID string) response.Response { t, err := f.getService(ctx) if err != nil { return errorToResponse(err) @@ -38,15 +38,15 @@ func (f *PrometheusApiHandler) handleRouteGetRuleStatuses(ctx *models.ReqContext return t.RouteGetRuleStatuses(ctx) } -func (f *PrometheusApiHandler) handleRouteGetGrafanaAlertStatuses(ctx *models.ReqContext) response.Response { +func (f *PrometheusApiHandler) handleRouteGetGrafanaAlertStatuses(ctx *contextmodel.ReqContext) response.Response { return f.GrafanaSvc.RouteGetAlertStatuses(ctx) } -func (f *PrometheusApiHandler) handleRouteGetGrafanaRuleStatuses(ctx *models.ReqContext) response.Response { +func (f *PrometheusApiHandler) handleRouteGetGrafanaRuleStatuses(ctx *contextmodel.ReqContext) response.Response { return f.GrafanaSvc.RouteGetRuleStatuses(ctx) } -func (f *PrometheusApiHandler) getService(ctx *models.ReqContext) (*LotexProm, error) { +func (f *PrometheusApiHandler) getService(ctx *contextmodel.ReqContext) (*LotexProm, error) { _, err := getDatasourceByUID(ctx, f.DatasourceCache, apimodels.LoTexRulerBackend) if err != nil { return nil, err diff --git a/pkg/services/ngalert/api/forking_ruler.go b/pkg/services/ngalert/api/forking_ruler.go index b415204bca4..e26afac7abb 100644 --- a/pkg/services/ngalert/api/forking_ruler.go +++ b/pkg/services/ngalert/api/forking_ruler.go @@ -2,7 +2,7 @@ package api import ( "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" ) @@ -22,7 +22,7 @@ func NewForkingRuler(datasourceCache datasources.CacheService, lotex *LotexRuler } } -func (f *RulerApiHandler) handleRouteDeleteNamespaceRulesConfig(ctx *models.ReqContext, dsUID, namespace string) response.Response { +func (f *RulerApiHandler) handleRouteDeleteNamespaceRulesConfig(ctx *contextmodel.ReqContext, dsUID, namespace string) response.Response { t, err := f.getService(ctx) if err != nil { return errorToResponse(err) @@ -30,7 +30,7 @@ func (f *RulerApiHandler) handleRouteDeleteNamespaceRulesConfig(ctx *models.ReqC return t.RouteDeleteNamespaceRulesConfig(ctx, namespace) } -func (f *RulerApiHandler) handleRouteDeleteRuleGroupConfig(ctx *models.ReqContext, dsUID, namespace, group string) response.Response { +func (f *RulerApiHandler) handleRouteDeleteRuleGroupConfig(ctx *contextmodel.ReqContext, dsUID, namespace, group string) response.Response { t, err := f.getService(ctx) if err != nil { return errorToResponse(err) @@ -38,7 +38,7 @@ func (f *RulerApiHandler) handleRouteDeleteRuleGroupConfig(ctx *models.ReqContex return t.RouteDeleteRuleGroupConfig(ctx, namespace, group) } -func (f *RulerApiHandler) handleRouteGetNamespaceRulesConfig(ctx *models.ReqContext, dsUID, namespace string) response.Response { +func (f *RulerApiHandler) handleRouteGetNamespaceRulesConfig(ctx *contextmodel.ReqContext, dsUID, namespace string) response.Response { t, err := f.getService(ctx) if err != nil { return errorToResponse(err) @@ -46,7 +46,7 @@ func (f *RulerApiHandler) handleRouteGetNamespaceRulesConfig(ctx *models.ReqCont return t.RouteGetNamespaceRulesConfig(ctx, namespace) } -func (f *RulerApiHandler) handleRouteGetRulegGroupConfig(ctx *models.ReqContext, dsUID, namespace, group string) response.Response { +func (f *RulerApiHandler) handleRouteGetRulegGroupConfig(ctx *contextmodel.ReqContext, dsUID, namespace, group string) response.Response { t, err := f.getService(ctx) if err != nil { return errorToResponse(err) @@ -54,7 +54,7 @@ func (f *RulerApiHandler) handleRouteGetRulegGroupConfig(ctx *models.ReqContext, return t.RouteGetRulegGroupConfig(ctx, namespace, group) } -func (f *RulerApiHandler) handleRouteGetRulesConfig(ctx *models.ReqContext, dsUID string) response.Response { +func (f *RulerApiHandler) handleRouteGetRulesConfig(ctx *contextmodel.ReqContext, dsUID string) response.Response { t, err := f.getService(ctx) if err != nil { return errorToResponse(err) @@ -62,7 +62,7 @@ func (f *RulerApiHandler) handleRouteGetRulesConfig(ctx *models.ReqContext, dsUI return t.RouteGetRulesConfig(ctx) } -func (f *RulerApiHandler) handleRoutePostNameRulesConfig(ctx *models.ReqContext, conf apimodels.PostableRuleGroupConfig, dsUID, namespace string) response.Response { +func (f *RulerApiHandler) handleRoutePostNameRulesConfig(ctx *contextmodel.ReqContext, conf apimodels.PostableRuleGroupConfig, dsUID, namespace string) response.Response { t, err := f.getService(ctx) if err != nil { return errorToResponse(err) @@ -73,27 +73,27 @@ func (f *RulerApiHandler) handleRoutePostNameRulesConfig(ctx *models.ReqContext, return t.RoutePostNameRulesConfig(ctx, conf, namespace) } -func (f *RulerApiHandler) handleRouteDeleteNamespaceGrafanaRulesConfig(ctx *models.ReqContext, namespace string) response.Response { +func (f *RulerApiHandler) handleRouteDeleteNamespaceGrafanaRulesConfig(ctx *contextmodel.ReqContext, namespace string) response.Response { return f.GrafanaRuler.RouteDeleteAlertRules(ctx, namespace, "") } -func (f *RulerApiHandler) handleRouteDeleteGrafanaRuleGroupConfig(ctx *models.ReqContext, namespace, groupName string) response.Response { +func (f *RulerApiHandler) handleRouteDeleteGrafanaRuleGroupConfig(ctx *contextmodel.ReqContext, namespace, groupName string) response.Response { return f.GrafanaRuler.RouteDeleteAlertRules(ctx, namespace, groupName) } -func (f *RulerApiHandler) handleRouteGetNamespaceGrafanaRulesConfig(ctx *models.ReqContext, namespace string) response.Response { +func (f *RulerApiHandler) handleRouteGetNamespaceGrafanaRulesConfig(ctx *contextmodel.ReqContext, namespace string) response.Response { return f.GrafanaRuler.RouteGetNamespaceRulesConfig(ctx, namespace) } -func (f *RulerApiHandler) handleRouteGetGrafanaRuleGroupConfig(ctx *models.ReqContext, namespace, group string) response.Response { +func (f *RulerApiHandler) handleRouteGetGrafanaRuleGroupConfig(ctx *contextmodel.ReqContext, namespace, group string) response.Response { return f.GrafanaRuler.RouteGetRulesGroupConfig(ctx, namespace, group) } -func (f *RulerApiHandler) handleRouteGetGrafanaRulesConfig(ctx *models.ReqContext) response.Response { +func (f *RulerApiHandler) handleRouteGetGrafanaRulesConfig(ctx *contextmodel.ReqContext) response.Response { return f.GrafanaRuler.RouteGetRulesConfig(ctx) } -func (f *RulerApiHandler) handleRoutePostNameGrafanaRulesConfig(ctx *models.ReqContext, conf apimodels.PostableRuleGroupConfig, namespace string) response.Response { +func (f *RulerApiHandler) handleRoutePostNameGrafanaRulesConfig(ctx *contextmodel.ReqContext, conf apimodels.PostableRuleGroupConfig, namespace string) response.Response { payloadType := conf.Type() if payloadType != apimodels.GrafanaBackend { return errorToResponse(backendTypeDoesNotMatchPayloadTypeError(apimodels.GrafanaBackend, conf.Type().String())) @@ -101,7 +101,7 @@ func (f *RulerApiHandler) handleRoutePostNameGrafanaRulesConfig(ctx *models.ReqC return f.GrafanaRuler.RoutePostNameRulesConfig(ctx, conf, namespace) } -func (f *RulerApiHandler) getService(ctx *models.ReqContext) (*LotexRuler, error) { +func (f *RulerApiHandler) getService(ctx *contextmodel.ReqContext) (*LotexRuler, error) { _, err := getDatasourceByUID(ctx, f.DatasourceCache, apimodels.LoTexRulerBackend) if err != nil { return nil, err diff --git a/pkg/services/ngalert/api/generated_base_api_alertmanager.go b/pkg/services/ngalert/api/generated_base_api_alertmanager.go index c6b35d35fe0..65ec0ad8b5a 100644 --- a/pkg/services/ngalert/api/generated_base_api_alertmanager.go +++ b/pkg/services/ngalert/api/generated_base_api_alertmanager.go @@ -12,39 +12,39 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/metrics" "github.com/grafana/grafana/pkg/web" ) type AlertmanagerApi interface { - RouteCreateGrafanaSilence(*models.ReqContext) response.Response - RouteCreateSilence(*models.ReqContext) response.Response - RouteDeleteAlertingConfig(*models.ReqContext) response.Response - RouteDeleteGrafanaAlertingConfig(*models.ReqContext) response.Response - RouteDeleteGrafanaSilence(*models.ReqContext) response.Response - RouteDeleteSilence(*models.ReqContext) response.Response - RouteGetAMAlertGroups(*models.ReqContext) response.Response - RouteGetAMAlerts(*models.ReqContext) response.Response - RouteGetAMStatus(*models.ReqContext) response.Response - RouteGetAlertingConfig(*models.ReqContext) response.Response - RouteGetGrafanaAMAlertGroups(*models.ReqContext) response.Response - RouteGetGrafanaAMAlerts(*models.ReqContext) response.Response - RouteGetGrafanaAMStatus(*models.ReqContext) response.Response - RouteGetGrafanaAlertingConfig(*models.ReqContext) response.Response - RouteGetGrafanaReceivers(*models.ReqContext) response.Response - RouteGetGrafanaSilence(*models.ReqContext) response.Response - RouteGetGrafanaSilences(*models.ReqContext) response.Response - RouteGetSilence(*models.ReqContext) response.Response - RouteGetSilences(*models.ReqContext) response.Response - RoutePostAMAlerts(*models.ReqContext) response.Response - RoutePostAlertingConfig(*models.ReqContext) response.Response - RoutePostGrafanaAlertingConfig(*models.ReqContext) response.Response - RoutePostTestGrafanaReceivers(*models.ReqContext) response.Response + RouteCreateGrafanaSilence(*contextmodel.ReqContext) response.Response + RouteCreateSilence(*contextmodel.ReqContext) response.Response + RouteDeleteAlertingConfig(*contextmodel.ReqContext) response.Response + RouteDeleteGrafanaAlertingConfig(*contextmodel.ReqContext) response.Response + RouteDeleteGrafanaSilence(*contextmodel.ReqContext) response.Response + RouteDeleteSilence(*contextmodel.ReqContext) response.Response + RouteGetAMAlertGroups(*contextmodel.ReqContext) response.Response + RouteGetAMAlerts(*contextmodel.ReqContext) response.Response + RouteGetAMStatus(*contextmodel.ReqContext) response.Response + RouteGetAlertingConfig(*contextmodel.ReqContext) response.Response + RouteGetGrafanaAMAlertGroups(*contextmodel.ReqContext) response.Response + RouteGetGrafanaAMAlerts(*contextmodel.ReqContext) response.Response + RouteGetGrafanaAMStatus(*contextmodel.ReqContext) response.Response + RouteGetGrafanaAlertingConfig(*contextmodel.ReqContext) response.Response + RouteGetGrafanaReceivers(*contextmodel.ReqContext) response.Response + RouteGetGrafanaSilence(*contextmodel.ReqContext) response.Response + RouteGetGrafanaSilences(*contextmodel.ReqContext) response.Response + RouteGetSilence(*contextmodel.ReqContext) response.Response + RouteGetSilences(*contextmodel.ReqContext) response.Response + RoutePostAMAlerts(*contextmodel.ReqContext) response.Response + RoutePostAlertingConfig(*contextmodel.ReqContext) response.Response + RoutePostGrafanaAlertingConfig(*contextmodel.ReqContext) response.Response + RoutePostTestGrafanaReceivers(*contextmodel.ReqContext) response.Response } -func (f *AlertmanagerApiHandler) RouteCreateGrafanaSilence(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RouteCreateGrafanaSilence(ctx *contextmodel.ReqContext) response.Response { // Parse Request Body conf := apimodels.PostableSilence{} if err := web.Bind(ctx.Req, &conf); err != nil { @@ -52,7 +52,7 @@ func (f *AlertmanagerApiHandler) RouteCreateGrafanaSilence(ctx *models.ReqContex } return f.handleRouteCreateGrafanaSilence(ctx, conf) } -func (f *AlertmanagerApiHandler) RouteCreateSilence(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RouteCreateSilence(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] // Parse Request Body @@ -62,80 +62,80 @@ func (f *AlertmanagerApiHandler) RouteCreateSilence(ctx *models.ReqContext) resp } return f.handleRouteCreateSilence(ctx, conf, datasourceUIDParam) } -func (f *AlertmanagerApiHandler) RouteDeleteAlertingConfig(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RouteDeleteAlertingConfig(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] return f.handleRouteDeleteAlertingConfig(ctx, datasourceUIDParam) } -func (f *AlertmanagerApiHandler) RouteDeleteGrafanaAlertingConfig(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RouteDeleteGrafanaAlertingConfig(ctx *contextmodel.ReqContext) response.Response { return f.handleRouteDeleteGrafanaAlertingConfig(ctx) } -func (f *AlertmanagerApiHandler) RouteDeleteGrafanaSilence(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RouteDeleteGrafanaSilence(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters silenceIdParam := web.Params(ctx.Req)[":SilenceId"] return f.handleRouteDeleteGrafanaSilence(ctx, silenceIdParam) } -func (f *AlertmanagerApiHandler) RouteDeleteSilence(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RouteDeleteSilence(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters silenceIdParam := web.Params(ctx.Req)[":SilenceId"] datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] return f.handleRouteDeleteSilence(ctx, silenceIdParam, datasourceUIDParam) } -func (f *AlertmanagerApiHandler) RouteGetAMAlertGroups(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RouteGetAMAlertGroups(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] return f.handleRouteGetAMAlertGroups(ctx, datasourceUIDParam) } -func (f *AlertmanagerApiHandler) RouteGetAMAlerts(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RouteGetAMAlerts(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] return f.handleRouteGetAMAlerts(ctx, datasourceUIDParam) } -func (f *AlertmanagerApiHandler) RouteGetAMStatus(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RouteGetAMStatus(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] return f.handleRouteGetAMStatus(ctx, datasourceUIDParam) } -func (f *AlertmanagerApiHandler) RouteGetAlertingConfig(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RouteGetAlertingConfig(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] return f.handleRouteGetAlertingConfig(ctx, datasourceUIDParam) } -func (f *AlertmanagerApiHandler) RouteGetGrafanaAMAlertGroups(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RouteGetGrafanaAMAlertGroups(ctx *contextmodel.ReqContext) response.Response { return f.handleRouteGetGrafanaAMAlertGroups(ctx) } -func (f *AlertmanagerApiHandler) RouteGetGrafanaAMAlerts(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RouteGetGrafanaAMAlerts(ctx *contextmodel.ReqContext) response.Response { return f.handleRouteGetGrafanaAMAlerts(ctx) } -func (f *AlertmanagerApiHandler) RouteGetGrafanaAMStatus(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RouteGetGrafanaAMStatus(ctx *contextmodel.ReqContext) response.Response { return f.handleRouteGetGrafanaAMStatus(ctx) } -func (f *AlertmanagerApiHandler) RouteGetGrafanaAlertingConfig(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RouteGetGrafanaAlertingConfig(ctx *contextmodel.ReqContext) response.Response { return f.handleRouteGetGrafanaAlertingConfig(ctx) } -func (f *AlertmanagerApiHandler) RouteGetGrafanaReceivers(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RouteGetGrafanaReceivers(ctx *contextmodel.ReqContext) response.Response { return f.handleRouteGetGrafanaReceivers(ctx) } -func (f *AlertmanagerApiHandler) RouteGetGrafanaSilence(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RouteGetGrafanaSilence(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters silenceIdParam := web.Params(ctx.Req)[":SilenceId"] return f.handleRouteGetGrafanaSilence(ctx, silenceIdParam) } -func (f *AlertmanagerApiHandler) RouteGetGrafanaSilences(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RouteGetGrafanaSilences(ctx *contextmodel.ReqContext) response.Response { return f.handleRouteGetGrafanaSilences(ctx) } -func (f *AlertmanagerApiHandler) RouteGetSilence(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RouteGetSilence(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters silenceIdParam := web.Params(ctx.Req)[":SilenceId"] datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] return f.handleRouteGetSilence(ctx, silenceIdParam, datasourceUIDParam) } -func (f *AlertmanagerApiHandler) RouteGetSilences(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RouteGetSilences(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] return f.handleRouteGetSilences(ctx, datasourceUIDParam) } -func (f *AlertmanagerApiHandler) RoutePostAMAlerts(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RoutePostAMAlerts(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] // Parse Request Body @@ -145,7 +145,7 @@ func (f *AlertmanagerApiHandler) RoutePostAMAlerts(ctx *models.ReqContext) respo } return f.handleRoutePostAMAlerts(ctx, conf, datasourceUIDParam) } -func (f *AlertmanagerApiHandler) RoutePostAlertingConfig(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RoutePostAlertingConfig(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] // Parse Request Body @@ -155,7 +155,7 @@ func (f *AlertmanagerApiHandler) RoutePostAlertingConfig(ctx *models.ReqContext) } return f.handleRoutePostAlertingConfig(ctx, conf, datasourceUIDParam) } -func (f *AlertmanagerApiHandler) RoutePostGrafanaAlertingConfig(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RoutePostGrafanaAlertingConfig(ctx *contextmodel.ReqContext) response.Response { // Parse Request Body conf := apimodels.PostableUserConfig{} if err := web.Bind(ctx.Req, &conf); err != nil { @@ -163,7 +163,7 @@ func (f *AlertmanagerApiHandler) RoutePostGrafanaAlertingConfig(ctx *models.ReqC } return f.handleRoutePostGrafanaAlertingConfig(ctx, conf) } -func (f *AlertmanagerApiHandler) RoutePostTestGrafanaReceivers(ctx *models.ReqContext) response.Response { +func (f *AlertmanagerApiHandler) RoutePostTestGrafanaReceivers(ctx *contextmodel.ReqContext) response.Response { // Parse Request Body conf := apimodels.TestReceiversConfigBodyParams{} if err := web.Bind(ctx.Req, &conf); err != nil { diff --git a/pkg/services/ngalert/api/generated_base_api_configuration.go b/pkg/services/ngalert/api/generated_base_api_configuration.go index 8a8912b96b5..c0685d12a1c 100644 --- a/pkg/services/ngalert/api/generated_base_api_configuration.go +++ b/pkg/services/ngalert/api/generated_base_api_configuration.go @@ -12,33 +12,33 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/metrics" "github.com/grafana/grafana/pkg/web" ) type ConfigurationApi interface { - RouteDeleteNGalertConfig(*models.ReqContext) response.Response - RouteGetAlertmanagers(*models.ReqContext) response.Response - RouteGetNGalertConfig(*models.ReqContext) response.Response - RouteGetStatus(*models.ReqContext) response.Response - RoutePostNGalertConfig(*models.ReqContext) response.Response + RouteDeleteNGalertConfig(*contextmodel.ReqContext) response.Response + RouteGetAlertmanagers(*contextmodel.ReqContext) response.Response + RouteGetNGalertConfig(*contextmodel.ReqContext) response.Response + RouteGetStatus(*contextmodel.ReqContext) response.Response + RoutePostNGalertConfig(*contextmodel.ReqContext) response.Response } -func (f *ConfigurationApiHandler) RouteDeleteNGalertConfig(ctx *models.ReqContext) response.Response { +func (f *ConfigurationApiHandler) RouteDeleteNGalertConfig(ctx *contextmodel.ReqContext) response.Response { return f.handleRouteDeleteNGalertConfig(ctx) } -func (f *ConfigurationApiHandler) RouteGetAlertmanagers(ctx *models.ReqContext) response.Response { +func (f *ConfigurationApiHandler) RouteGetAlertmanagers(ctx *contextmodel.ReqContext) response.Response { return f.handleRouteGetAlertmanagers(ctx) } -func (f *ConfigurationApiHandler) RouteGetNGalertConfig(ctx *models.ReqContext) response.Response { +func (f *ConfigurationApiHandler) RouteGetNGalertConfig(ctx *contextmodel.ReqContext) response.Response { return f.handleRouteGetNGalertConfig(ctx) } -func (f *ConfigurationApiHandler) RouteGetStatus(ctx *models.ReqContext) response.Response { +func (f *ConfigurationApiHandler) RouteGetStatus(ctx *contextmodel.ReqContext) response.Response { return f.handleRouteGetStatus(ctx) } -func (f *ConfigurationApiHandler) RoutePostNGalertConfig(ctx *models.ReqContext) response.Response { +func (f *ConfigurationApiHandler) RoutePostNGalertConfig(ctx *contextmodel.ReqContext) response.Response { // Parse Request Body conf := apimodels.PostableNGalertConfig{} if err := web.Bind(ctx.Req, &conf); err != nil { diff --git a/pkg/services/ngalert/api/generated_base_api_prometheus.go b/pkg/services/ngalert/api/generated_base_api_prometheus.go index 05297027bc3..a4f04e25199 100644 --- a/pkg/services/ngalert/api/generated_base_api_prometheus.go +++ b/pkg/services/ngalert/api/generated_base_api_prometheus.go @@ -12,30 +12,30 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/ngalert/metrics" "github.com/grafana/grafana/pkg/web" ) type PrometheusApi interface { - RouteGetAlertStatuses(*models.ReqContext) response.Response - RouteGetGrafanaAlertStatuses(*models.ReqContext) response.Response - RouteGetGrafanaRuleStatuses(*models.ReqContext) response.Response - RouteGetRuleStatuses(*models.ReqContext) response.Response + RouteGetAlertStatuses(*contextmodel.ReqContext) response.Response + RouteGetGrafanaAlertStatuses(*contextmodel.ReqContext) response.Response + RouteGetGrafanaRuleStatuses(*contextmodel.ReqContext) response.Response + RouteGetRuleStatuses(*contextmodel.ReqContext) response.Response } -func (f *PrometheusApiHandler) RouteGetAlertStatuses(ctx *models.ReqContext) response.Response { +func (f *PrometheusApiHandler) RouteGetAlertStatuses(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] return f.handleRouteGetAlertStatuses(ctx, datasourceUIDParam) } -func (f *PrometheusApiHandler) RouteGetGrafanaAlertStatuses(ctx *models.ReqContext) response.Response { +func (f *PrometheusApiHandler) RouteGetGrafanaAlertStatuses(ctx *contextmodel.ReqContext) response.Response { return f.handleRouteGetGrafanaAlertStatuses(ctx) } -func (f *PrometheusApiHandler) RouteGetGrafanaRuleStatuses(ctx *models.ReqContext) response.Response { +func (f *PrometheusApiHandler) RouteGetGrafanaRuleStatuses(ctx *contextmodel.ReqContext) response.Response { return f.handleRouteGetGrafanaRuleStatuses(ctx) } -func (f *PrometheusApiHandler) RouteGetRuleStatuses(ctx *models.ReqContext) response.Response { +func (f *PrometheusApiHandler) RouteGetRuleStatuses(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] return f.handleRouteGetRuleStatuses(ctx, datasourceUIDParam) diff --git a/pkg/services/ngalert/api/generated_base_api_provisioning.go b/pkg/services/ngalert/api/generated_base_api_provisioning.go index d7d8c8b9776..f3e6ed66c32 100644 --- a/pkg/services/ngalert/api/generated_base_api_provisioning.go +++ b/pkg/services/ngalert/api/generated_base_api_provisioning.go @@ -12,95 +12,112 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/metrics" "github.com/grafana/grafana/pkg/web" ) type ProvisioningApi interface { - RouteDeleteAlertRule(*models.ReqContext) response.Response - RouteDeleteContactpoints(*models.ReqContext) response.Response - RouteDeleteMuteTiming(*models.ReqContext) response.Response - RouteDeleteTemplate(*models.ReqContext) response.Response - RouteGetAlertRule(*models.ReqContext) response.Response - RouteGetAlertRuleGroup(*models.ReqContext) response.Response - RouteGetAlertRules(*models.ReqContext) response.Response - RouteGetContactpoints(*models.ReqContext) response.Response - RouteGetMuteTiming(*models.ReqContext) response.Response - RouteGetMuteTimings(*models.ReqContext) response.Response - RouteGetPolicyTree(*models.ReqContext) response.Response - RouteGetTemplate(*models.ReqContext) response.Response - RouteGetTemplates(*models.ReqContext) response.Response - RoutePostAlertRule(*models.ReqContext) response.Response - RoutePostContactpoints(*models.ReqContext) response.Response - RoutePostMuteTiming(*models.ReqContext) response.Response - RoutePutAlertRule(*models.ReqContext) response.Response - RoutePutAlertRuleGroup(*models.ReqContext) response.Response - RoutePutContactpoint(*models.ReqContext) response.Response - RoutePutMuteTiming(*models.ReqContext) response.Response - RoutePutPolicyTree(*models.ReqContext) response.Response - RoutePutTemplate(*models.ReqContext) response.Response - RouteResetPolicyTree(*models.ReqContext) response.Response + RouteDeleteAlertRule(*contextmodel.ReqContext) response.Response + RouteDeleteContactpoints(*contextmodel.ReqContext) response.Response + RouteDeleteMuteTiming(*contextmodel.ReqContext) response.Response + RouteDeleteTemplate(*contextmodel.ReqContext) response.Response + RouteGetAlertRule(*contextmodel.ReqContext) response.Response + RouteGetAlertRuleExport(*contextmodel.ReqContext) response.Response + RouteGetAlertRuleGroup(*contextmodel.ReqContext) response.Response + RouteGetAlertRuleGroupExport(*contextmodel.ReqContext) response.Response + RouteGetAlertRules(*contextmodel.ReqContext) response.Response + RouteGetAlertRulesExport(*contextmodel.ReqContext) response.Response + RouteGetContactpoints(*contextmodel.ReqContext) response.Response + RouteGetMuteTiming(*contextmodel.ReqContext) response.Response + RouteGetMuteTimings(*contextmodel.ReqContext) response.Response + RouteGetPolicyTree(*contextmodel.ReqContext) response.Response + RouteGetTemplate(*contextmodel.ReqContext) response.Response + RouteGetTemplates(*contextmodel.ReqContext) response.Response + RoutePostAlertRule(*contextmodel.ReqContext) response.Response + RoutePostContactpoints(*contextmodel.ReqContext) response.Response + RoutePostMuteTiming(*contextmodel.ReqContext) response.Response + RoutePutAlertRule(*contextmodel.ReqContext) response.Response + RoutePutAlertRuleGroup(*contextmodel.ReqContext) response.Response + RoutePutContactpoint(*contextmodel.ReqContext) response.Response + RoutePutMuteTiming(*contextmodel.ReqContext) response.Response + RoutePutPolicyTree(*contextmodel.ReqContext) response.Response + RoutePutTemplate(*contextmodel.ReqContext) response.Response + RouteResetPolicyTree(*contextmodel.ReqContext) response.Response } -func (f *ProvisioningApiHandler) RouteDeleteAlertRule(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RouteDeleteAlertRule(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters uIDParam := web.Params(ctx.Req)[":UID"] return f.handleRouteDeleteAlertRule(ctx, uIDParam) } -func (f *ProvisioningApiHandler) RouteDeleteContactpoints(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RouteDeleteContactpoints(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters uIDParam := web.Params(ctx.Req)[":UID"] return f.handleRouteDeleteContactpoints(ctx, uIDParam) } -func (f *ProvisioningApiHandler) RouteDeleteMuteTiming(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RouteDeleteMuteTiming(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters nameParam := web.Params(ctx.Req)[":name"] return f.handleRouteDeleteMuteTiming(ctx, nameParam) } -func (f *ProvisioningApiHandler) RouteDeleteTemplate(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RouteDeleteTemplate(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters nameParam := web.Params(ctx.Req)[":name"] return f.handleRouteDeleteTemplate(ctx, nameParam) } -func (f *ProvisioningApiHandler) RouteGetAlertRule(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RouteGetAlertRule(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters uIDParam := web.Params(ctx.Req)[":UID"] return f.handleRouteGetAlertRule(ctx, uIDParam) } -func (f *ProvisioningApiHandler) RouteGetAlertRuleGroup(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RouteGetAlertRuleExport(ctx *contextmodel.ReqContext) response.Response { + // Parse Path Parameters + uIDParam := web.Params(ctx.Req)[":UID"] + return f.handleRouteGetAlertRuleExport(ctx, uIDParam) +} +func (f *ProvisioningApiHandler) RouteGetAlertRuleGroup(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters folderUIDParam := web.Params(ctx.Req)[":FolderUID"] groupParam := web.Params(ctx.Req)[":Group"] return f.handleRouteGetAlertRuleGroup(ctx, folderUIDParam, groupParam) } -func (f *ProvisioningApiHandler) RouteGetAlertRules(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RouteGetAlertRuleGroupExport(ctx *contextmodel.ReqContext) response.Response { + // Parse Path Parameters + folderUIDParam := web.Params(ctx.Req)[":FolderUID"] + groupParam := web.Params(ctx.Req)[":Group"] + return f.handleRouteGetAlertRuleGroupExport(ctx, folderUIDParam, groupParam) +} +func (f *ProvisioningApiHandler) RouteGetAlertRules(ctx *contextmodel.ReqContext) response.Response { return f.handleRouteGetAlertRules(ctx) } -func (f *ProvisioningApiHandler) RouteGetContactpoints(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RouteGetAlertRulesExport(ctx *contextmodel.ReqContext) response.Response { + return f.handleRouteGetAlertRulesExport(ctx) +} +func (f *ProvisioningApiHandler) RouteGetContactpoints(ctx *contextmodel.ReqContext) response.Response { return f.handleRouteGetContactpoints(ctx) } -func (f *ProvisioningApiHandler) RouteGetMuteTiming(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RouteGetMuteTiming(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters nameParam := web.Params(ctx.Req)[":name"] return f.handleRouteGetMuteTiming(ctx, nameParam) } -func (f *ProvisioningApiHandler) RouteGetMuteTimings(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RouteGetMuteTimings(ctx *contextmodel.ReqContext) response.Response { return f.handleRouteGetMuteTimings(ctx) } -func (f *ProvisioningApiHandler) RouteGetPolicyTree(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RouteGetPolicyTree(ctx *contextmodel.ReqContext) response.Response { return f.handleRouteGetPolicyTree(ctx) } -func (f *ProvisioningApiHandler) RouteGetTemplate(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RouteGetTemplate(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters nameParam := web.Params(ctx.Req)[":name"] return f.handleRouteGetTemplate(ctx, nameParam) } -func (f *ProvisioningApiHandler) RouteGetTemplates(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RouteGetTemplates(ctx *contextmodel.ReqContext) response.Response { return f.handleRouteGetTemplates(ctx) } -func (f *ProvisioningApiHandler) RoutePostAlertRule(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RoutePostAlertRule(ctx *contextmodel.ReqContext) response.Response { // Parse Request Body conf := apimodels.ProvisionedAlertRule{} if err := web.Bind(ctx.Req, &conf); err != nil { @@ -108,7 +125,7 @@ func (f *ProvisioningApiHandler) RoutePostAlertRule(ctx *models.ReqContext) resp } return f.handleRoutePostAlertRule(ctx, conf) } -func (f *ProvisioningApiHandler) RoutePostContactpoints(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RoutePostContactpoints(ctx *contextmodel.ReqContext) response.Response { // Parse Request Body conf := apimodels.EmbeddedContactPoint{} if err := web.Bind(ctx.Req, &conf); err != nil { @@ -116,7 +133,7 @@ func (f *ProvisioningApiHandler) RoutePostContactpoints(ctx *models.ReqContext) } return f.handleRoutePostContactpoints(ctx, conf) } -func (f *ProvisioningApiHandler) RoutePostMuteTiming(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RoutePostMuteTiming(ctx *contextmodel.ReqContext) response.Response { // Parse Request Body conf := apimodels.MuteTimeInterval{} if err := web.Bind(ctx.Req, &conf); err != nil { @@ -124,7 +141,7 @@ func (f *ProvisioningApiHandler) RoutePostMuteTiming(ctx *models.ReqContext) res } return f.handleRoutePostMuteTiming(ctx, conf) } -func (f *ProvisioningApiHandler) RoutePutAlertRule(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RoutePutAlertRule(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters uIDParam := web.Params(ctx.Req)[":UID"] // Parse Request Body @@ -134,7 +151,7 @@ func (f *ProvisioningApiHandler) RoutePutAlertRule(ctx *models.ReqContext) respo } return f.handleRoutePutAlertRule(ctx, conf, uIDParam) } -func (f *ProvisioningApiHandler) RoutePutAlertRuleGroup(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RoutePutAlertRuleGroup(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters folderUIDParam := web.Params(ctx.Req)[":FolderUID"] groupParam := web.Params(ctx.Req)[":Group"] @@ -145,7 +162,7 @@ func (f *ProvisioningApiHandler) RoutePutAlertRuleGroup(ctx *models.ReqContext) } return f.handleRoutePutAlertRuleGroup(ctx, conf, folderUIDParam, groupParam) } -func (f *ProvisioningApiHandler) RoutePutContactpoint(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RoutePutContactpoint(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters uIDParam := web.Params(ctx.Req)[":UID"] // Parse Request Body @@ -155,7 +172,7 @@ func (f *ProvisioningApiHandler) RoutePutContactpoint(ctx *models.ReqContext) re } return f.handleRoutePutContactpoint(ctx, conf, uIDParam) } -func (f *ProvisioningApiHandler) RoutePutMuteTiming(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RoutePutMuteTiming(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters nameParam := web.Params(ctx.Req)[":name"] // Parse Request Body @@ -165,7 +182,7 @@ func (f *ProvisioningApiHandler) RoutePutMuteTiming(ctx *models.ReqContext) resp } return f.handleRoutePutMuteTiming(ctx, conf, nameParam) } -func (f *ProvisioningApiHandler) RoutePutPolicyTree(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RoutePutPolicyTree(ctx *contextmodel.ReqContext) response.Response { // Parse Request Body conf := apimodels.Route{} if err := web.Bind(ctx.Req, &conf); err != nil { @@ -173,7 +190,7 @@ func (f *ProvisioningApiHandler) RoutePutPolicyTree(ctx *models.ReqContext) resp } return f.handleRoutePutPolicyTree(ctx, conf) } -func (f *ProvisioningApiHandler) RoutePutTemplate(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RoutePutTemplate(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters nameParam := web.Params(ctx.Req)[":name"] // Parse Request Body @@ -183,7 +200,7 @@ func (f *ProvisioningApiHandler) RoutePutTemplate(ctx *models.ReqContext) respon } return f.handleRoutePutTemplate(ctx, conf, nameParam) } -func (f *ProvisioningApiHandler) RouteResetPolicyTree(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) RouteResetPolicyTree(ctx *contextmodel.ReqContext) response.Response { return f.handleRouteResetPolicyTree(ctx) } @@ -239,6 +256,16 @@ func (api *API) RegisterProvisioningApiEndpoints(srv ProvisioningApi, m *metrics m, ), ) + group.Get( + toMacaronPath("/api/v1/provisioning/alert-rules/{UID}/export"), + api.authorize(http.MethodGet, "/api/v1/provisioning/alert-rules/{UID}/export"), + metrics.Instrument( + http.MethodGet, + "/api/v1/provisioning/alert-rules/{UID}/export", + srv.RouteGetAlertRuleExport, + m, + ), + ) group.Get( toMacaronPath("/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}"), api.authorize(http.MethodGet, "/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}"), @@ -249,6 +276,16 @@ func (api *API) RegisterProvisioningApiEndpoints(srv ProvisioningApi, m *metrics m, ), ) + group.Get( + toMacaronPath("/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}/export"), + api.authorize(http.MethodGet, "/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}/export"), + metrics.Instrument( + http.MethodGet, + "/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}/export", + srv.RouteGetAlertRuleGroupExport, + m, + ), + ) group.Get( toMacaronPath("/api/v1/provisioning/alert-rules"), api.authorize(http.MethodGet, "/api/v1/provisioning/alert-rules"), @@ -259,6 +296,16 @@ func (api *API) RegisterProvisioningApiEndpoints(srv ProvisioningApi, m *metrics m, ), ) + group.Get( + toMacaronPath("/api/v1/provisioning/alert-rules/export"), + api.authorize(http.MethodGet, "/api/v1/provisioning/alert-rules/export"), + metrics.Instrument( + http.MethodGet, + "/api/v1/provisioning/alert-rules/export", + srv.RouteGetAlertRulesExport, + m, + ), + ) group.Get( toMacaronPath("/api/v1/provisioning/contact-points"), api.authorize(http.MethodGet, "/api/v1/provisioning/contact-points"), diff --git a/pkg/services/ngalert/api/generated_base_api_ruler.go b/pkg/services/ngalert/api/generated_base_api_ruler.go index ac584615874..b5687db2a3f 100644 --- a/pkg/services/ngalert/api/generated_base_api_ruler.go +++ b/pkg/services/ngalert/api/generated_base_api_ruler.go @@ -12,84 +12,84 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/metrics" "github.com/grafana/grafana/pkg/web" ) type RulerApi interface { - RouteDeleteGrafanaRuleGroupConfig(*models.ReqContext) response.Response - RouteDeleteNamespaceGrafanaRulesConfig(*models.ReqContext) response.Response - RouteDeleteNamespaceRulesConfig(*models.ReqContext) response.Response - RouteDeleteRuleGroupConfig(*models.ReqContext) response.Response - RouteGetGrafanaRuleGroupConfig(*models.ReqContext) response.Response - RouteGetGrafanaRulesConfig(*models.ReqContext) response.Response - RouteGetNamespaceGrafanaRulesConfig(*models.ReqContext) response.Response - RouteGetNamespaceRulesConfig(*models.ReqContext) response.Response - RouteGetRulegGroupConfig(*models.ReqContext) response.Response - RouteGetRulesConfig(*models.ReqContext) response.Response - RoutePostNameGrafanaRulesConfig(*models.ReqContext) response.Response - RoutePostNameRulesConfig(*models.ReqContext) response.Response + RouteDeleteGrafanaRuleGroupConfig(*contextmodel.ReqContext) response.Response + RouteDeleteNamespaceGrafanaRulesConfig(*contextmodel.ReqContext) response.Response + RouteDeleteNamespaceRulesConfig(*contextmodel.ReqContext) response.Response + RouteDeleteRuleGroupConfig(*contextmodel.ReqContext) response.Response + RouteGetGrafanaRuleGroupConfig(*contextmodel.ReqContext) response.Response + RouteGetGrafanaRulesConfig(*contextmodel.ReqContext) response.Response + RouteGetNamespaceGrafanaRulesConfig(*contextmodel.ReqContext) response.Response + RouteGetNamespaceRulesConfig(*contextmodel.ReqContext) response.Response + RouteGetRulegGroupConfig(*contextmodel.ReqContext) response.Response + RouteGetRulesConfig(*contextmodel.ReqContext) response.Response + RoutePostNameGrafanaRulesConfig(*contextmodel.ReqContext) response.Response + RoutePostNameRulesConfig(*contextmodel.ReqContext) response.Response } -func (f *RulerApiHandler) RouteDeleteGrafanaRuleGroupConfig(ctx *models.ReqContext) response.Response { +func (f *RulerApiHandler) RouteDeleteGrafanaRuleGroupConfig(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters namespaceParam := web.Params(ctx.Req)[":Namespace"] groupnameParam := web.Params(ctx.Req)[":Groupname"] return f.handleRouteDeleteGrafanaRuleGroupConfig(ctx, namespaceParam, groupnameParam) } -func (f *RulerApiHandler) RouteDeleteNamespaceGrafanaRulesConfig(ctx *models.ReqContext) response.Response { +func (f *RulerApiHandler) RouteDeleteNamespaceGrafanaRulesConfig(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters namespaceParam := web.Params(ctx.Req)[":Namespace"] return f.handleRouteDeleteNamespaceGrafanaRulesConfig(ctx, namespaceParam) } -func (f *RulerApiHandler) RouteDeleteNamespaceRulesConfig(ctx *models.ReqContext) response.Response { +func (f *RulerApiHandler) RouteDeleteNamespaceRulesConfig(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] namespaceParam := web.Params(ctx.Req)[":Namespace"] return f.handleRouteDeleteNamespaceRulesConfig(ctx, datasourceUIDParam, namespaceParam) } -func (f *RulerApiHandler) RouteDeleteRuleGroupConfig(ctx *models.ReqContext) response.Response { +func (f *RulerApiHandler) RouteDeleteRuleGroupConfig(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] namespaceParam := web.Params(ctx.Req)[":Namespace"] groupnameParam := web.Params(ctx.Req)[":Groupname"] return f.handleRouteDeleteRuleGroupConfig(ctx, datasourceUIDParam, namespaceParam, groupnameParam) } -func (f *RulerApiHandler) RouteGetGrafanaRuleGroupConfig(ctx *models.ReqContext) response.Response { +func (f *RulerApiHandler) RouteGetGrafanaRuleGroupConfig(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters namespaceParam := web.Params(ctx.Req)[":Namespace"] groupnameParam := web.Params(ctx.Req)[":Groupname"] return f.handleRouteGetGrafanaRuleGroupConfig(ctx, namespaceParam, groupnameParam) } -func (f *RulerApiHandler) RouteGetGrafanaRulesConfig(ctx *models.ReqContext) response.Response { +func (f *RulerApiHandler) RouteGetGrafanaRulesConfig(ctx *contextmodel.ReqContext) response.Response { return f.handleRouteGetGrafanaRulesConfig(ctx) } -func (f *RulerApiHandler) RouteGetNamespaceGrafanaRulesConfig(ctx *models.ReqContext) response.Response { +func (f *RulerApiHandler) RouteGetNamespaceGrafanaRulesConfig(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters namespaceParam := web.Params(ctx.Req)[":Namespace"] return f.handleRouteGetNamespaceGrafanaRulesConfig(ctx, namespaceParam) } -func (f *RulerApiHandler) RouteGetNamespaceRulesConfig(ctx *models.ReqContext) response.Response { +func (f *RulerApiHandler) RouteGetNamespaceRulesConfig(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] namespaceParam := web.Params(ctx.Req)[":Namespace"] return f.handleRouteGetNamespaceRulesConfig(ctx, datasourceUIDParam, namespaceParam) } -func (f *RulerApiHandler) RouteGetRulegGroupConfig(ctx *models.ReqContext) response.Response { +func (f *RulerApiHandler) RouteGetRulegGroupConfig(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] namespaceParam := web.Params(ctx.Req)[":Namespace"] groupnameParam := web.Params(ctx.Req)[":Groupname"] return f.handleRouteGetRulegGroupConfig(ctx, datasourceUIDParam, namespaceParam, groupnameParam) } -func (f *RulerApiHandler) RouteGetRulesConfig(ctx *models.ReqContext) response.Response { +func (f *RulerApiHandler) RouteGetRulesConfig(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] return f.handleRouteGetRulesConfig(ctx, datasourceUIDParam) } -func (f *RulerApiHandler) RoutePostNameGrafanaRulesConfig(ctx *models.ReqContext) response.Response { +func (f *RulerApiHandler) RoutePostNameGrafanaRulesConfig(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters namespaceParam := web.Params(ctx.Req)[":Namespace"] // Parse Request Body @@ -99,7 +99,7 @@ func (f *RulerApiHandler) RoutePostNameGrafanaRulesConfig(ctx *models.ReqContext } return f.handleRoutePostNameGrafanaRulesConfig(ctx, conf, namespaceParam) } -func (f *RulerApiHandler) RoutePostNameRulesConfig(ctx *models.ReqContext) response.Response { +func (f *RulerApiHandler) RoutePostNameRulesConfig(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] namespaceParam := web.Params(ctx.Req)[":Namespace"] diff --git a/pkg/services/ngalert/api/generated_base_api_testing.go b/pkg/services/ngalert/api/generated_base_api_testing.go index 6018bc58344..6b3e104efab 100644 --- a/pkg/services/ngalert/api/generated_base_api_testing.go +++ b/pkg/services/ngalert/api/generated_base_api_testing.go @@ -12,20 +12,20 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/metrics" "github.com/grafana/grafana/pkg/web" ) type TestingApi interface { - BacktestConfig(*models.ReqContext) response.Response - RouteEvalQueries(*models.ReqContext) response.Response - RouteTestRuleConfig(*models.ReqContext) response.Response - RouteTestRuleGrafanaConfig(*models.ReqContext) response.Response + BacktestConfig(*contextmodel.ReqContext) response.Response + RouteEvalQueries(*contextmodel.ReqContext) response.Response + RouteTestRuleConfig(*contextmodel.ReqContext) response.Response + RouteTestRuleGrafanaConfig(*contextmodel.ReqContext) response.Response } -func (f *TestingApiHandler) BacktestConfig(ctx *models.ReqContext) response.Response { +func (f *TestingApiHandler) BacktestConfig(ctx *contextmodel.ReqContext) response.Response { // Parse Request Body conf := apimodels.BacktestConfig{} if err := web.Bind(ctx.Req, &conf); err != nil { @@ -33,7 +33,7 @@ func (f *TestingApiHandler) BacktestConfig(ctx *models.ReqContext) response.Resp } return f.handleBacktestConfig(ctx, conf) } -func (f *TestingApiHandler) RouteEvalQueries(ctx *models.ReqContext) response.Response { +func (f *TestingApiHandler) RouteEvalQueries(ctx *contextmodel.ReqContext) response.Response { // Parse Request Body conf := apimodels.EvalQueriesPayload{} if err := web.Bind(ctx.Req, &conf); err != nil { @@ -41,7 +41,7 @@ func (f *TestingApiHandler) RouteEvalQueries(ctx *models.ReqContext) response.Re } return f.handleRouteEvalQueries(ctx, conf) } -func (f *TestingApiHandler) RouteTestRuleConfig(ctx *models.ReqContext) response.Response { +func (f *TestingApiHandler) RouteTestRuleConfig(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"] // Parse Request Body @@ -51,7 +51,7 @@ func (f *TestingApiHandler) RouteTestRuleConfig(ctx *models.ReqContext) response } return f.handleRouteTestRuleConfig(ctx, conf, datasourceUIDParam) } -func (f *TestingApiHandler) RouteTestRuleGrafanaConfig(ctx *models.ReqContext) response.Response { +func (f *TestingApiHandler) RouteTestRuleGrafanaConfig(ctx *contextmodel.ReqContext) response.Response { // Parse Request Body conf := apimodels.TestRulePayload{} if err := web.Bind(ctx.Req, &conf); err != nil { diff --git a/pkg/services/ngalert/api/lotex_am.go b/pkg/services/ngalert/api/lotex_am.go index 9db6b947767..0a4ac25864e 100644 --- a/pkg/services/ngalert/api/lotex_am.go +++ b/pkg/services/ngalert/api/lotex_am.go @@ -12,7 +12,7 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/web" @@ -61,7 +61,7 @@ func NewLotexAM(proxy *AlertingProxy, log log.Logger) *LotexAM { } func (am *LotexAM) withAMReq( - ctx *models.ReqContext, + ctx *contextmodel.ReqContext, method string, endpoint string, pathParams []string, @@ -110,7 +110,7 @@ func (am *LotexAM) withAMReq( ) } -func (am *LotexAM) RouteGetAMStatus(ctx *models.ReqContext) response.Response { +func (am *LotexAM) RouteGetAMStatus(ctx *contextmodel.ReqContext) response.Response { return am.withAMReq( ctx, http.MethodGet, @@ -122,7 +122,7 @@ func (am *LotexAM) RouteGetAMStatus(ctx *models.ReqContext) response.Response { ) } -func (am *LotexAM) RouteCreateSilence(ctx *models.ReqContext, silenceBody apimodels.PostableSilence) response.Response { +func (am *LotexAM) RouteCreateSilence(ctx *contextmodel.ReqContext, silenceBody apimodels.PostableSilence) response.Response { blob, err := json.Marshal(silenceBody) if err != nil { return ErrResp(500, err, "Failed marshal silence") @@ -138,7 +138,7 @@ func (am *LotexAM) RouteCreateSilence(ctx *models.ReqContext, silenceBody apimod ) } -func (am *LotexAM) RouteDeleteAlertingConfig(ctx *models.ReqContext) response.Response { +func (am *LotexAM) RouteDeleteAlertingConfig(ctx *contextmodel.ReqContext) response.Response { return am.withAMReq( ctx, http.MethodDelete, @@ -150,7 +150,7 @@ func (am *LotexAM) RouteDeleteAlertingConfig(ctx *models.ReqContext) response.Re ) } -func (am *LotexAM) RouteDeleteSilence(ctx *models.ReqContext, silenceID string) response.Response { +func (am *LotexAM) RouteDeleteSilence(ctx *contextmodel.ReqContext, silenceID string) response.Response { return am.withAMReq( ctx, http.MethodDelete, @@ -162,7 +162,7 @@ func (am *LotexAM) RouteDeleteSilence(ctx *models.ReqContext, silenceID string) ) } -func (am *LotexAM) RouteGetAlertingConfig(ctx *models.ReqContext) response.Response { +func (am *LotexAM) RouteGetAlertingConfig(ctx *contextmodel.ReqContext) response.Response { return am.withAMReq( ctx, http.MethodGet, @@ -174,7 +174,7 @@ func (am *LotexAM) RouteGetAlertingConfig(ctx *models.ReqContext) response.Respo ) } -func (am *LotexAM) RouteGetAMAlertGroups(ctx *models.ReqContext) response.Response { +func (am *LotexAM) RouteGetAMAlertGroups(ctx *contextmodel.ReqContext) response.Response { return am.withAMReq( ctx, http.MethodGet, @@ -186,7 +186,7 @@ func (am *LotexAM) RouteGetAMAlertGroups(ctx *models.ReqContext) response.Respon ) } -func (am *LotexAM) RouteGetAMAlerts(ctx *models.ReqContext) response.Response { +func (am *LotexAM) RouteGetAMAlerts(ctx *contextmodel.ReqContext) response.Response { return am.withAMReq( ctx, http.MethodGet, @@ -198,7 +198,7 @@ func (am *LotexAM) RouteGetAMAlerts(ctx *models.ReqContext) response.Response { ) } -func (am *LotexAM) RouteGetSilence(ctx *models.ReqContext, silenceID string) response.Response { +func (am *LotexAM) RouteGetSilence(ctx *contextmodel.ReqContext, silenceID string) response.Response { return am.withAMReq( ctx, http.MethodGet, @@ -210,7 +210,7 @@ func (am *LotexAM) RouteGetSilence(ctx *models.ReqContext, silenceID string) res ) } -func (am *LotexAM) RouteGetSilences(ctx *models.ReqContext) response.Response { +func (am *LotexAM) RouteGetSilences(ctx *contextmodel.ReqContext) response.Response { return am.withAMReq( ctx, http.MethodGet, @@ -222,7 +222,7 @@ func (am *LotexAM) RouteGetSilences(ctx *models.ReqContext) response.Response { ) } -func (am *LotexAM) RoutePostAlertingConfig(ctx *models.ReqContext, config apimodels.PostableUserConfig) response.Response { +func (am *LotexAM) RoutePostAlertingConfig(ctx *contextmodel.ReqContext, config apimodels.PostableUserConfig) response.Response { yml, err := yaml.Marshal(&config) if err != nil { return ErrResp(500, err, "Failed marshal alert manager configuration ") @@ -239,7 +239,7 @@ func (am *LotexAM) RoutePostAlertingConfig(ctx *models.ReqContext, config apimod ) } -func (am *LotexAM) RoutePostAMAlerts(ctx *models.ReqContext, alerts apimodels.PostableAlerts) response.Response { +func (am *LotexAM) RoutePostAMAlerts(ctx *contextmodel.ReqContext, alerts apimodels.PostableAlerts) response.Response { yml, err := yaml.Marshal(alerts) if err != nil { return ErrResp(500, err, "Failed marshal postable alerts") diff --git a/pkg/services/ngalert/api/lotex_prom.go b/pkg/services/ngalert/api/lotex_prom.go index f94797a4294..e3be8525bb1 100644 --- a/pkg/services/ngalert/api/lotex_prom.go +++ b/pkg/services/ngalert/api/lotex_prom.go @@ -6,7 +6,7 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/web" ) @@ -38,7 +38,7 @@ func NewLotexProm(proxy *AlertingProxy, log log.Logger) *LotexProm { } } -func (p *LotexProm) RouteGetAlertStatuses(ctx *models.ReqContext) response.Response { +func (p *LotexProm) RouteGetAlertStatuses(ctx *contextmodel.ReqContext) response.Response { endpoints, err := p.getEndpoints(ctx) if err != nil { return ErrResp(http.StatusInternalServerError, err, "") @@ -57,7 +57,7 @@ func (p *LotexProm) RouteGetAlertStatuses(ctx *models.ReqContext) response.Respo ) } -func (p *LotexProm) RouteGetRuleStatuses(ctx *models.ReqContext) response.Response { +func (p *LotexProm) RouteGetRuleStatuses(ctx *contextmodel.ReqContext) response.Response { endpoints, err := p.getEndpoints(ctx) if err != nil { return ErrResp(http.StatusInternalServerError, err, "") @@ -76,7 +76,7 @@ func (p *LotexProm) RouteGetRuleStatuses(ctx *models.ReqContext) response.Respon ) } -func (p *LotexProm) getEndpoints(ctx *models.ReqContext) (*promEndpoints, error) { +func (p *LotexProm) getEndpoints(ctx *contextmodel.ReqContext) (*promEndpoints, error) { datasourceUID := web.Params(ctx.Req)[":DatasourceUID"] if datasourceUID == "" { return nil, fmt.Errorf("datasource UID is invalid") diff --git a/pkg/services/ngalert/api/lotex_ruler.go b/pkg/services/ngalert/api/lotex_ruler.go index 1b1dc49289c..5a6a3905cc2 100644 --- a/pkg/services/ngalert/api/lotex_ruler.go +++ b/pkg/services/ngalert/api/lotex_ruler.go @@ -6,13 +6,13 @@ import ( "net/http" "net/url" - apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" - "github.com/grafana/grafana/pkg/web" "gopkg.in/yaml.v3" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" + apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + "github.com/grafana/grafana/pkg/web" ) const ( @@ -55,7 +55,7 @@ func NewLotexRuler(proxy *AlertingProxy, log log.Logger) *LotexRuler { } } -func (r *LotexRuler) RouteDeleteNamespaceRulesConfig(ctx *models.ReqContext, namespace string) response.Response { +func (r *LotexRuler) RouteDeleteNamespaceRulesConfig(ctx *contextmodel.ReqContext, namespace string) response.Response { legacyRulerPrefix, err := r.validateAndGetPrefix(ctx) if err != nil { return ErrResp(500, err, "") @@ -73,7 +73,7 @@ func (r *LotexRuler) RouteDeleteNamespaceRulesConfig(ctx *models.ReqContext, nam ) } -func (r *LotexRuler) RouteDeleteRuleGroupConfig(ctx *models.ReqContext, namespace string, group string) response.Response { +func (r *LotexRuler) RouteDeleteRuleGroupConfig(ctx *contextmodel.ReqContext, namespace string, group string) response.Response { legacyRulerPrefix, err := r.validateAndGetPrefix(ctx) if err != nil { return ErrResp(500, err, "") @@ -96,7 +96,7 @@ func (r *LotexRuler) RouteDeleteRuleGroupConfig(ctx *models.ReqContext, namespac ) } -func (r *LotexRuler) RouteGetNamespaceRulesConfig(ctx *models.ReqContext, namespace string) response.Response { +func (r *LotexRuler) RouteGetNamespaceRulesConfig(ctx *contextmodel.ReqContext, namespace string) response.Response { legacyRulerPrefix, err := r.validateAndGetPrefix(ctx) if err != nil { return ErrResp(500, err, "") @@ -118,7 +118,7 @@ func (r *LotexRuler) RouteGetNamespaceRulesConfig(ctx *models.ReqContext, namesp ) } -func (r *LotexRuler) RouteGetRulegGroupConfig(ctx *models.ReqContext, namespace string, group string) response.Response { +func (r *LotexRuler) RouteGetRulegGroupConfig(ctx *contextmodel.ReqContext, namespace string, group string) response.Response { legacyRulerPrefix, err := r.validateAndGetPrefix(ctx) if err != nil { return ErrResp(500, err, "") @@ -141,7 +141,7 @@ func (r *LotexRuler) RouteGetRulegGroupConfig(ctx *models.ReqContext, namespace ) } -func (r *LotexRuler) RouteGetRulesConfig(ctx *models.ReqContext) response.Response { +func (r *LotexRuler) RouteGetRulesConfig(ctx *contextmodel.ReqContext) response.Response { legacyRulerPrefix, err := r.validateAndGetPrefix(ctx) if err != nil { return ErrResp(500, err, "") @@ -160,7 +160,7 @@ func (r *LotexRuler) RouteGetRulesConfig(ctx *models.ReqContext) response.Respon ) } -func (r *LotexRuler) RoutePostNameRulesConfig(ctx *models.ReqContext, conf apimodels.PostableRuleGroupConfig, ns string) response.Response { +func (r *LotexRuler) RoutePostNameRulesConfig(ctx *contextmodel.ReqContext, conf apimodels.PostableRuleGroupConfig, ns string) response.Response { legacyRulerPrefix, err := r.validateAndGetPrefix(ctx) if err != nil { return ErrResp(500, err, "") @@ -173,7 +173,7 @@ func (r *LotexRuler) RoutePostNameRulesConfig(ctx *models.ReqContext, conf apimo return r.withReq(ctx, http.MethodPost, u, bytes.NewBuffer(yml), jsonExtractor(nil), nil) } -func (r *LotexRuler) validateAndGetPrefix(ctx *models.ReqContext) (string, error) { +func (r *LotexRuler) validateAndGetPrefix(ctx *contextmodel.ReqContext) (string, error) { datasourceUID := web.Params(ctx.Req)[":DatasourceUID"] if datasourceUID == "" { return "", fmt.Errorf("datasource UID is invalid") diff --git a/pkg/services/ngalert/api/lotex_ruler_test.go b/pkg/services/ngalert/api/lotex_ruler_test.go index a9800a90406..8631782e358 100644 --- a/pkg/services/ngalert/api/lotex_ruler_test.go +++ b/pkg/services/ngalert/api/lotex_ruler_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasourceproxy" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/user" @@ -98,7 +98,7 @@ func TestLotexRuler_ValidateAndGetPrefix(t *testing.T) { // Setup request context. httpReq, err := http.NewRequest(http.MethodGet, "http://grafanacloud.com"+tt.urlParams, nil) require.NoError(t, err) - ctx := &models.ReqContext{Context: &web.Context{Req: web.SetURLParams(httpReq, tt.namedParams)}} + ctx := &contextmodel.ReqContext{Context: &web.Context{Req: web.SetURLParams(httpReq, tt.namedParams)}} prefix, err := ruler.validateAndGetPrefix(ctx) require.Equal(t, tt.expected, prefix) diff --git a/pkg/services/ngalert/api/promql_compat.go b/pkg/services/ngalert/api/promql_compat.go index 889771ccb23..0b4cb2b621b 100644 --- a/pkg/services/ngalert/api/promql_compat.go +++ b/pkg/services/ngalert/api/promql_compat.go @@ -9,12 +9,12 @@ import ( cortex_util "github.com/cortexproject/cortex/pkg/util" "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/prometheus/prometheus/pkg/labels" "github.com/prometheus/prometheus/promql" "github.com/prometheus/prometheus/promql/parser" "github.com/grafana/grafana/pkg/api/response" + "github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/grafana/grafana/pkg/util" ) diff --git a/pkg/services/ngalert/api/provisioning.go b/pkg/services/ngalert/api/provisioning.go index 6dc74c75c1c..d26b6bb5e75 100644 --- a/pkg/services/ngalert/api/provisioning.go +++ b/pkg/services/ngalert/api/provisioning.go @@ -2,7 +2,7 @@ package api import ( "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" ) @@ -16,94 +16,106 @@ func NewProvisioningApi(svc *ProvisioningSrv) *ProvisioningApiHandler { } } -func (f *ProvisioningApiHandler) handleRouteGetPolicyTree(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) handleRouteGetPolicyTree(ctx *contextmodel.ReqContext) response.Response { return f.svc.RouteGetPolicyTree(ctx) } -func (f *ProvisioningApiHandler) handleRoutePutPolicyTree(ctx *models.ReqContext, route apimodels.Route) response.Response { +func (f *ProvisioningApiHandler) handleRoutePutPolicyTree(ctx *contextmodel.ReqContext, route apimodels.Route) response.Response { return f.svc.RoutePutPolicyTree(ctx, route) } -func (f *ProvisioningApiHandler) handleRouteGetContactpoints(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) handleRouteGetContactpoints(ctx *contextmodel.ReqContext) response.Response { return f.svc.RouteGetContactPoints(ctx) } -func (f *ProvisioningApiHandler) handleRoutePostContactpoints(ctx *models.ReqContext, cp apimodels.EmbeddedContactPoint) response.Response { +func (f *ProvisioningApiHandler) handleRoutePostContactpoints(ctx *contextmodel.ReqContext, cp apimodels.EmbeddedContactPoint) response.Response { return f.svc.RoutePostContactPoint(ctx, cp) } -func (f *ProvisioningApiHandler) handleRoutePutContactpoint(ctx *models.ReqContext, cp apimodels.EmbeddedContactPoint, UID string) response.Response { +func (f *ProvisioningApiHandler) handleRoutePutContactpoint(ctx *contextmodel.ReqContext, cp apimodels.EmbeddedContactPoint, UID string) response.Response { return f.svc.RoutePutContactPoint(ctx, cp, UID) } -func (f *ProvisioningApiHandler) handleRouteDeleteContactpoints(ctx *models.ReqContext, UID string) response.Response { +func (f *ProvisioningApiHandler) handleRouteDeleteContactpoints(ctx *contextmodel.ReqContext, UID string) response.Response { return f.svc.RouteDeleteContactPoint(ctx, UID) } -func (f *ProvisioningApiHandler) handleRouteGetTemplates(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) handleRouteGetTemplates(ctx *contextmodel.ReqContext) response.Response { return f.svc.RouteGetTemplates(ctx) } -func (f *ProvisioningApiHandler) handleRouteGetTemplate(ctx *models.ReqContext, name string) response.Response { +func (f *ProvisioningApiHandler) handleRouteGetTemplate(ctx *contextmodel.ReqContext, name string) response.Response { return f.svc.RouteGetTemplate(ctx, name) } -func (f *ProvisioningApiHandler) handleRoutePutTemplate(ctx *models.ReqContext, body apimodels.NotificationTemplateContent, name string) response.Response { +func (f *ProvisioningApiHandler) handleRoutePutTemplate(ctx *contextmodel.ReqContext, body apimodels.NotificationTemplateContent, name string) response.Response { return f.svc.RoutePutTemplate(ctx, body, name) } -func (f *ProvisioningApiHandler) handleRouteDeleteTemplate(ctx *models.ReqContext, name string) response.Response { +func (f *ProvisioningApiHandler) handleRouteDeleteTemplate(ctx *contextmodel.ReqContext, name string) response.Response { return f.svc.RouteDeleteTemplate(ctx, name) } -func (f *ProvisioningApiHandler) handleRouteGetMuteTiming(ctx *models.ReqContext, name string) response.Response { +func (f *ProvisioningApiHandler) handleRouteGetMuteTiming(ctx *contextmodel.ReqContext, name string) response.Response { return f.svc.RouteGetMuteTiming(ctx, name) } -func (f *ProvisioningApiHandler) handleRouteGetMuteTimings(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) handleRouteGetMuteTimings(ctx *contextmodel.ReqContext) response.Response { return f.svc.RouteGetMuteTimings(ctx) } -func (f *ProvisioningApiHandler) handleRoutePostMuteTiming(ctx *models.ReqContext, mt apimodels.MuteTimeInterval) response.Response { +func (f *ProvisioningApiHandler) handleRoutePostMuteTiming(ctx *contextmodel.ReqContext, mt apimodels.MuteTimeInterval) response.Response { return f.svc.RoutePostMuteTiming(ctx, mt) } -func (f *ProvisioningApiHandler) handleRoutePutMuteTiming(ctx *models.ReqContext, mt apimodels.MuteTimeInterval, name string) response.Response { +func (f *ProvisioningApiHandler) handleRoutePutMuteTiming(ctx *contextmodel.ReqContext, mt apimodels.MuteTimeInterval, name string) response.Response { return f.svc.RoutePutMuteTiming(ctx, mt, name) } -func (f *ProvisioningApiHandler) handleRouteDeleteMuteTiming(ctx *models.ReqContext, name string) response.Response { +func (f *ProvisioningApiHandler) handleRouteDeleteMuteTiming(ctx *contextmodel.ReqContext, name string) response.Response { return f.svc.RouteDeleteMuteTiming(ctx, name) } -func (f *ProvisioningApiHandler) handleRouteGetAlertRules(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) handleRouteGetAlertRules(ctx *contextmodel.ReqContext) response.Response { return f.svc.RouteGetAlertRules(ctx) } -func (f *ProvisioningApiHandler) handleRouteGetAlertRule(ctx *models.ReqContext, UID string) response.Response { +func (f *ProvisioningApiHandler) handleRouteGetAlertRule(ctx *contextmodel.ReqContext, UID string) response.Response { return f.svc.RouteRouteGetAlertRule(ctx, UID) } -func (f *ProvisioningApiHandler) handleRoutePostAlertRule(ctx *models.ReqContext, ar apimodels.ProvisionedAlertRule) response.Response { +func (f *ProvisioningApiHandler) handleRouteGetAlertRuleExport(ctx *contextmodel.ReqContext, UID string) response.Response { + return f.svc.RouteGetAlertRuleExport(ctx, UID) +} + +func (f *ProvisioningApiHandler) handleRouteGetAlertRulesExport(ctx *contextmodel.ReqContext) response.Response { + return f.svc.RouteGetAlertRulesExport(ctx) +} + +func (f *ProvisioningApiHandler) handleRoutePostAlertRule(ctx *contextmodel.ReqContext, ar apimodels.ProvisionedAlertRule) response.Response { return f.svc.RoutePostAlertRule(ctx, ar) } -func (f *ProvisioningApiHandler) handleRoutePutAlertRule(ctx *models.ReqContext, ar apimodels.ProvisionedAlertRule, UID string) response.Response { +func (f *ProvisioningApiHandler) handleRoutePutAlertRule(ctx *contextmodel.ReqContext, ar apimodels.ProvisionedAlertRule, UID string) response.Response { return f.svc.RoutePutAlertRule(ctx, ar, UID) } -func (f *ProvisioningApiHandler) handleRouteDeleteAlertRule(ctx *models.ReqContext, UID string) response.Response { +func (f *ProvisioningApiHandler) handleRouteDeleteAlertRule(ctx *contextmodel.ReqContext, UID string) response.Response { return f.svc.RouteDeleteAlertRule(ctx, UID) } -func (f *ProvisioningApiHandler) handleRouteResetPolicyTree(ctx *models.ReqContext) response.Response { +func (f *ProvisioningApiHandler) handleRouteResetPolicyTree(ctx *contextmodel.ReqContext) response.Response { return f.svc.RouteResetPolicyTree(ctx) } -func (f *ProvisioningApiHandler) handleRouteGetAlertRuleGroup(ctx *models.ReqContext, folder, group string) response.Response { +func (f *ProvisioningApiHandler) handleRouteGetAlertRuleGroup(ctx *contextmodel.ReqContext, folder, group string) response.Response { return f.svc.RouteGetAlertRuleGroup(ctx, folder, group) } -func (f *ProvisioningApiHandler) handleRoutePutAlertRuleGroup(ctx *models.ReqContext, ag apimodels.AlertRuleGroup, folder, group string) response.Response { +func (f *ProvisioningApiHandler) handleRouteGetAlertRuleGroupExport(ctx *contextmodel.ReqContext, folder, group string) response.Response { + return f.svc.RouteGetAlertRuleGroupExport(ctx, folder, group) +} + +func (f *ProvisioningApiHandler) handleRoutePutAlertRuleGroup(ctx *contextmodel.ReqContext, ag apimodels.AlertRuleGroup, folder, group string) response.Response { return f.svc.RoutePutAlertRuleGroup(ctx, ag, folder, group) } diff --git a/pkg/services/ngalert/api/testing.go b/pkg/services/ngalert/api/testing.go index 68504135781..d2a72db8529 100644 --- a/pkg/services/ngalert/api/testing.go +++ b/pkg/services/ngalert/api/testing.go @@ -6,10 +6,10 @@ import ( "testing" "time" + "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/grafana/grafana/pkg/services/ngalert/state" - - "github.com/grafana/grafana-plugin-sdk-go/data" ) type fakeAlertInstanceManager struct { diff --git a/pkg/services/ngalert/api/testing_api.go b/pkg/services/ngalert/api/testing_api.go index 8d3094f8709..23da0884de3 100644 --- a/pkg/services/ngalert/api/testing_api.go +++ b/pkg/services/ngalert/api/testing_api.go @@ -2,7 +2,7 @@ package api import ( "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" ) @@ -17,18 +17,18 @@ func NewTestingApi(svc *TestingApiSrv) *TestingApiHandler { } } -func (f *TestingApiHandler) handleRouteTestRuleConfig(c *models.ReqContext, body apimodels.TestRulePayload, dsUID string) response.Response { +func (f *TestingApiHandler) handleRouteTestRuleConfig(c *contextmodel.ReqContext, body apimodels.TestRulePayload, dsUID string) response.Response { return f.svc.RouteTestRuleConfig(c, body, dsUID) } -func (f *TestingApiHandler) handleRouteTestRuleGrafanaConfig(c *models.ReqContext, body apimodels.TestRulePayload) response.Response { +func (f *TestingApiHandler) handleRouteTestRuleGrafanaConfig(c *contextmodel.ReqContext, body apimodels.TestRulePayload) response.Response { return f.svc.RouteTestGrafanaRuleConfig(c, body) } -func (f *TestingApiHandler) handleRouteEvalQueries(c *models.ReqContext, body apimodels.EvalQueriesPayload) response.Response { +func (f *TestingApiHandler) handleRouteEvalQueries(c *contextmodel.ReqContext, body apimodels.EvalQueriesPayload) response.Response { return f.svc.RouteEvalQueries(c, body) } -func (f *TestingApiHandler) handleBacktestConfig(ctx *models.ReqContext, conf apimodels.BacktestConfig) response.Response { +func (f *TestingApiHandler) handleBacktestConfig(ctx *contextmodel.ReqContext, conf apimodels.BacktestConfig) response.Response { return f.svc.BacktestAlertRule(ctx, conf) } diff --git a/pkg/services/ngalert/api/tooling/api.json b/pkg/services/ngalert/api/tooling/api.json index 86cd1cc9299..09c49c9cbe3 100644 --- a/pkg/services/ngalert/api/tooling/api.json +++ b/pkg/services/ngalert/api/tooling/api.json @@ -121,6 +121,28 @@ "title": "AlertQuery represents a single query associated with an alert definition.", "type": "object" }, + "AlertQueryExport": { + "properties": { + "datasourceUid": { + "type": "string" + }, + "model": { + "additionalProperties": {}, + "type": "object" + }, + "queryType": { + "type": "string" + }, + "refId": { + "type": "string" + }, + "relativeTimeRange": { + "$ref": "#/definitions/RelativeTimeRange" + } + }, + "title": "AlertQueryExport is the provisioned export of models.AlertQuery.", + "type": "object" + }, "AlertResponse": { "properties": { "data": { @@ -141,6 +163,68 @@ ], "type": "object" }, + "AlertRuleExport": { + "properties": { + "annotations": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "condition": { + "type": "string" + }, + "dasboardUid": { + "type": "string" + }, + "data": { + "items": { + "$ref": "#/definitions/AlertQueryExport" + }, + "type": "array" + }, + "execErrState": { + "enum": [ + "Alerting", + "Error", + "OK" + ], + "type": "string" + }, + "for": { + "$ref": "#/definitions/Duration" + }, + "isPaused": { + "type": "boolean" + }, + "labels": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "noDataState": { + "enum": [ + "Alerting", + "NoData", + "OK" + ], + "type": "string" + }, + "panelId": { + "format": "int64", + "type": "integer" + }, + "title": { + "type": "string" + }, + "uid": { + "type": "string" + } + }, + "title": "AlertRuleExport is the provisioned file export of models.AlertRule.", + "type": "object" + }, "AlertRuleGroup": { "properties": { "folderUid": { @@ -162,6 +246,31 @@ }, "type": "object" }, + "AlertRuleGroupExport": { + "properties": { + "folder": { + "type": "string" + }, + "interval": { + "$ref": "#/definitions/Duration" + }, + "name": { + "type": "string" + }, + "orgId": { + "format": "int64", + "type": "integer" + }, + "rules": { + "items": { + "$ref": "#/definitions/AlertRuleExport" + }, + "type": "array" + } + }, + "title": "AlertRuleGroupExport is the provisioned file export of AlertRuleGroupV1.", + "type": "object" + }, "AlertRuleGroupMetadata": { "properties": { "interval": { @@ -171,6 +280,22 @@ }, "type": "object" }, + "AlertingFileExport": { + "properties": { + "apiVersion": { + "format": "int64", + "type": "integer" + }, + "groups": { + "items": { + "$ref": "#/definitions/AlertRuleGroupExport" + }, + "type": "array" + } + }, + "title": "AlertingFileExport is the full provisioned file export.", + "type": "object" + }, "AlertingRule": { "description": "adapted from cortex", "properties": { @@ -2154,6 +2279,10 @@ "format": "int64", "type": "integer" }, + "isPaused": { + "example": false, + "type": "boolean" + }, "labels": { "additionalProperties": { "type": "string" @@ -3495,7 +3624,6 @@ "type": "object" }, "gettableAlert": { - "description": "GettableAlert gettable alert", "properties": { "annotations": { "$ref": "#/definitions/labelSet" @@ -3557,6 +3685,7 @@ "type": "array" }, "gettableSilence": { + "description": "GettableSilence gettable silence", "properties": { "comment": { "description": "comment", @@ -3755,7 +3884,6 @@ "type": "array" }, "postableSilence": { - "description": "PostableSilence postable silence", "properties": { "comment": { "description": "comment", @@ -3793,6 +3921,7 @@ "type": "object" }, "receiver": { + "description": "Receiver receiver", "properties": { "active": { "description": "active", @@ -3967,6 +4096,35 @@ ] } }, + "/api/v1/provisioning/alert-rules/export": { + "get": { + "operationId": "RouteGetAlertRulesExport", + "parameters": [ + { + "default": false, + "description": "Whether to initiate a download of the file or not.", + "in": "query", + "name": "download", + "type": "boolean" + } + ], + "responses": { + "200": { + "description": "AlertingFileExport", + "schema": { + "$ref": "#/definitions/AlertingFileExport" + } + }, + "404": { + "description": " Not found." + } + }, + "summary": "Export all alert rules in provisioning file format.", + "tags": [ + "provisioning" + ] + } + }, "/api/v1/provisioning/alert-rules/{UID}": { "delete": { "operationId": "RouteDeleteAlertRule", @@ -4062,6 +4220,47 @@ ] } }, + "/api/v1/provisioning/alert-rules/{UID}/export": { + "get": { + "operationId": "RouteGetAlertRuleExport", + "parameters": [ + { + "description": "Alert rule UID", + "in": "path", + "name": "UID", + "required": true, + "type": "string" + }, + { + "default": false, + "description": "Whether to initiate a download of the file or not.", + "in": "query", + "name": "download", + "type": "boolean" + } + ], + "produces": [ + "application/json", + "application/yaml", + "text/yaml" + ], + "responses": { + "200": { + "description": "AlertingFileExport", + "schema": { + "$ref": "#/definitions/AlertingFileExport" + } + }, + "404": { + "description": " Not found." + } + }, + "summary": "Export an alert rule in provisioning file format.", + "tags": [ + "provisioning" + ] + } + }, "/api/v1/provisioning/contact-points": { "get": { "operationId": "RouteGetContactpoints", @@ -4265,6 +4464,52 @@ ] } }, + "/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}/export": { + "get": { + "operationId": "RouteGetAlertRuleGroupExport", + "parameters": [ + { + "in": "path", + "name": "FolderUID", + "required": true, + "type": "string" + }, + { + "in": "path", + "name": "Group", + "required": true, + "type": "string" + }, + { + "default": false, + "description": "Whether to initiate a download of the file or not.", + "in": "query", + "name": "download", + "type": "boolean" + } + ], + "produces": [ + "application/json", + "application/yaml", + "text/yaml" + ], + "responses": { + "200": { + "description": "AlertingFileExport", + "schema": { + "$ref": "#/definitions/AlertingFileExport" + } + }, + "404": { + "description": " Not found." + } + }, + "summary": "Export an alert rule group in provisioning file format.", + "tags": [ + "provisioning" + ] + } + }, "/api/v1/provisioning/mute-timings": { "get": { "operationId": "RouteGetMuteTimings", diff --git a/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go b/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go index 8bf8c656a5c..d273de06201 100644 --- a/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go +++ b/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go @@ -3,8 +3,10 @@ package definitions import ( "time" - "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/prometheus/common/model" + + "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/services/provisioning/alerting/file" ) // swagger:route GET /api/v1/provisioning/alert-rules provisioning stable RouteGetAlertRules @@ -14,6 +16,14 @@ import ( // Responses: // 200: ProvisionedAlertRules +// swagger:route GET /api/v1/provisioning/alert-rules/export provisioning stable RouteGetAlertRulesExport +// +// Export all alert rules in provisioning file format. +// +// Responses: +// 200: AlertingFileExport +// 404: description: Not found. + // swagger:route GET /api/v1/provisioning/alert-rules/{UID} provisioning stable RouteGetAlertRule // // Get a specific alert rule by UID. @@ -22,6 +32,19 @@ import ( // 200: ProvisionedAlertRule // 404: description: Not found. +// swagger:route GET /api/v1/provisioning/alert-rules/{UID}/export provisioning stable RouteGetAlertRuleExport +// +// Export an alert rule in provisioning file format. +// +// Produces: +// - application/json +// - application/yaml +// - text/yaml +// +// Responses: +// 200: AlertingFileExport +// 404: description: Not found. + // swagger:route POST /api/v1/provisioning/alert-rules provisioning stable RoutePostAlertRule // // Create a new alert rule. @@ -51,7 +74,7 @@ import ( // Responses: // 204: description: The alert rule was deleted successfully. -// swagger:parameters RouteGetAlertRule RoutePutAlertRule RouteDeleteAlertRule +// swagger:parameters RouteGetAlertRule RoutePutAlertRule RouteDeleteAlertRule RouteGetAlertRuleExport type AlertRuleUIDReference struct { // Alert rule UID // in:path @@ -111,6 +134,8 @@ type ProvisionedAlertRule struct { Labels map[string]string `json:"labels,omitempty"` // readonly: true Provenance models.Provenance `json:"provenance,omitempty"` + // example: false + IsPaused bool `json:"isPaused"` } func (a *ProvisionedAlertRule) UpstreamModel() (models.AlertRule, error) { @@ -129,6 +154,7 @@ func (a *ProvisionedAlertRule) UpstreamModel() (models.AlertRule, error) { For: time.Duration(a.For), Annotations: a.Annotations, Labels: a.Labels, + IsPaused: a.IsPaused, }, nil } @@ -149,6 +175,7 @@ func NewAlertRule(rule models.AlertRule, provenance models.Provenance) Provision Annotations: rule.Annotations, Labels: rule.Labels, Provenance: provenance, + IsPaused: rule.IsPaused, } } @@ -168,6 +195,19 @@ func NewAlertRules(rules []*models.AlertRule) ProvisionedAlertRules { // 200: AlertRuleGroup // 404: description: Not found. +// swagger:route GET /api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}/export provisioning stable RouteGetAlertRuleGroupExport +// +// Export an alert rule group in provisioning file format. +// +// Produces: +// - application/json +// - application/yaml +// - text/yaml +// +// Responses: +// 200: AlertingFileExport +// 404: description: Not found. + // swagger:route PUT /api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group} provisioning stable RoutePutAlertRuleGroup // // Update the interval of a rule group. @@ -179,13 +219,13 @@ func NewAlertRules(rules []*models.AlertRule) ProvisionedAlertRules { // 200: AlertRuleGroup // 400: ValidationError -// swagger:parameters RouteGetAlertRuleGroup RoutePutAlertRuleGroup +// swagger:parameters RouteGetAlertRuleGroup RoutePutAlertRuleGroup RouteGetAlertRuleGroupExport type FolderUIDPathParam struct { // in:path FolderUID string `json:"FolderUID"` } -// swagger:parameters RouteGetAlertRuleGroup RoutePutAlertRuleGroup +// swagger:parameters RouteGetAlertRuleGroup RoutePutAlertRuleGroup RouteGetAlertRuleGroupExport type RuleGroupPathParam struct { // in:path Group string `json:"Group"` @@ -202,6 +242,15 @@ type AlertRuleGroupMetadata struct { Interval int64 `json:"interval"` } +// swagger:parameters RouteGetAlertRuleGroupExport RouteGetAlertRuleExport RouteGetAlertRulesExport +type ExportQueryParams struct { + // Whether to initiate a download of the file or not. + // in: query + // required: false + // default: false + Download bool `json:"download"` +} + // swagger:model type AlertRuleGroup struct { Title string `json:"title"` @@ -210,6 +259,10 @@ type AlertRuleGroup struct { Rules []ProvisionedAlertRule `json:"rules"` } +// AlertingFileExport is the full provisioned file export. +// swagger:model +type AlertingFileExport = file.AlertingFileExport + func (a *AlertRuleGroup) ToModel() (models.AlertRuleGroup, error) { ruleGroup := models.AlertRuleGroup{ Title: a.Title, diff --git a/pkg/services/ngalert/api/tooling/definitions/provisioning_mute_timings.go b/pkg/services/ngalert/api/tooling/definitions/provisioning_mute_timings.go index 9331d72b484..99d15fd2989 100644 --- a/pkg/services/ngalert/api/tooling/definitions/provisioning_mute_timings.go +++ b/pkg/services/ngalert/api/tooling/definitions/provisioning_mute_timings.go @@ -1,8 +1,9 @@ package definitions import ( - "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/prometheus/alertmanager/config" + + "github.com/grafana/grafana/pkg/services/ngalert/models" ) // swagger:route GET /api/v1/provisioning/mute-timings provisioning stable RouteGetMuteTimings diff --git a/pkg/services/ngalert/api/tooling/definitions/testing.go b/pkg/services/ngalert/api/tooling/definitions/testing.go index 4195d5fd3f7..2fb3738e4de 100644 --- a/pkg/services/ngalert/api/tooling/definitions/testing.go +++ b/pkg/services/ngalert/api/tooling/definitions/testing.go @@ -7,9 +7,8 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/prometheus/common/model" - "github.com/prometheus/alertmanager/config" + "github.com/prometheus/common/model" "github.com/prometheus/prometheus/promql" "github.com/grafana/grafana/pkg/services/ngalert/models" diff --git a/pkg/services/ngalert/api/tooling/post.json b/pkg/services/ngalert/api/tooling/post.json index ce003de9465..12c7b075536 100644 --- a/pkg/services/ngalert/api/tooling/post.json +++ b/pkg/services/ngalert/api/tooling/post.json @@ -121,6 +121,28 @@ "title": "AlertQuery represents a single query associated with an alert definition.", "type": "object" }, + "AlertQueryExport": { + "properties": { + "datasourceUid": { + "type": "string" + }, + "model": { + "additionalProperties": {}, + "type": "object" + }, + "queryType": { + "type": "string" + }, + "refId": { + "type": "string" + }, + "relativeTimeRange": { + "$ref": "#/definitions/RelativeTimeRange" + } + }, + "title": "AlertQueryExport is the provisioned export of models.AlertQuery.", + "type": "object" + }, "AlertResponse": { "properties": { "data": { @@ -141,6 +163,68 @@ ], "type": "object" }, + "AlertRuleExport": { + "properties": { + "annotations": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "condition": { + "type": "string" + }, + "dasboardUid": { + "type": "string" + }, + "data": { + "items": { + "$ref": "#/definitions/AlertQueryExport" + }, + "type": "array" + }, + "execErrState": { + "enum": [ + "Alerting", + "Error", + "OK" + ], + "type": "string" + }, + "for": { + "$ref": "#/definitions/Duration" + }, + "isPaused": { + "type": "boolean" + }, + "labels": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "noDataState": { + "enum": [ + "Alerting", + "NoData", + "OK" + ], + "type": "string" + }, + "panelId": { + "format": "int64", + "type": "integer" + }, + "title": { + "type": "string" + }, + "uid": { + "type": "string" + } + }, + "title": "AlertRuleExport is the provisioned file export of models.AlertRule.", + "type": "object" + }, "AlertRuleGroup": { "properties": { "folderUid": { @@ -162,6 +246,31 @@ }, "type": "object" }, + "AlertRuleGroupExport": { + "properties": { + "folder": { + "type": "string" + }, + "interval": { + "$ref": "#/definitions/Duration" + }, + "name": { + "type": "string" + }, + "orgId": { + "format": "int64", + "type": "integer" + }, + "rules": { + "items": { + "$ref": "#/definitions/AlertRuleExport" + }, + "type": "array" + } + }, + "title": "AlertRuleGroupExport is the provisioned file export of AlertRuleGroupV1.", + "type": "object" + }, "AlertRuleGroupMetadata": { "properties": { "interval": { @@ -171,6 +280,22 @@ }, "type": "object" }, + "AlertingFileExport": { + "properties": { + "apiVersion": { + "format": "int64", + "type": "integer" + }, + "groups": { + "items": { + "$ref": "#/definitions/AlertRuleGroupExport" + }, + "type": "array" + } + }, + "title": "AlertingFileExport is the full provisioned file export.", + "type": "object" + }, "AlertingRule": { "description": "adapted from cortex", "properties": { @@ -2154,6 +2279,10 @@ "format": "int64", "type": "integer" }, + "isPaused": { + "example": false, + "type": "boolean" + }, "labels": { "additionalProperties": { "type": "string" @@ -3155,6 +3284,7 @@ "type": "object" }, "URL": { + "description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use RawPath, an optional field which only gets\nset if the default encoding is different from Path.\n\nURL's String method uses the EscapedPath method to obtain the path. See the\nEscapedPath method for more details.", "properties": { "ForceQuery": { "type": "boolean" @@ -3190,7 +3320,7 @@ "$ref": "#/definitions/Userinfo" } }, - "title": "URL is a custom URL type that allows validation at configuration load time.", + "title": "A URL represents a parsed URL (technically, a URI reference).", "type": "object" }, "Userinfo": { @@ -3496,7 +3626,6 @@ "type": "object" }, "gettableAlert": { - "description": "GettableAlert gettable alert", "properties": { "annotations": { "$ref": "#/definitions/labelSet" @@ -3606,13 +3735,13 @@ "type": "object" }, "gettableSilences": { + "description": "GettableSilences gettable silences", "items": { "$ref": "#/definitions/gettableSilence" }, "type": "array" }, "integration": { - "description": "Integration integration", "properties": { "lastNotifyAttempt": { "description": "A timestamp indicating the last attempt to deliver a notification regardless of the outcome.\nFormat: date-time", @@ -3756,6 +3885,7 @@ "type": "array" }, "postableSilence": { + "description": "PostableSilence postable silence", "properties": { "comment": { "description": "comment", @@ -3793,7 +3923,6 @@ "type": "object" }, "receiver": { - "description": "Receiver receiver", "properties": { "active": { "description": "active", @@ -5640,6 +5769,35 @@ ] } }, + "/api/v1/provisioning/alert-rules/export": { + "get": { + "operationId": "RouteGetAlertRulesExport", + "parameters": [ + { + "default": false, + "description": "Whether to initiate a download of the file or not.", + "in": "query", + "name": "download", + "type": "boolean" + } + ], + "responses": { + "200": { + "description": "AlertingFileExport", + "schema": { + "$ref": "#/definitions/AlertingFileExport" + } + }, + "404": { + "description": " Not found." + } + }, + "summary": "Export all alert rules in provisioning file format.", + "tags": [ + "provisioning" + ] + } + }, "/api/v1/provisioning/alert-rules/{UID}": { "delete": { "operationId": "RouteDeleteAlertRule", @@ -5735,6 +5893,47 @@ ] } }, + "/api/v1/provisioning/alert-rules/{UID}/export": { + "get": { + "operationId": "RouteGetAlertRuleExport", + "parameters": [ + { + "description": "Alert rule UID", + "in": "path", + "name": "UID", + "required": true, + "type": "string" + }, + { + "default": false, + "description": "Whether to initiate a download of the file or not.", + "in": "query", + "name": "download", + "type": "boolean" + } + ], + "produces": [ + "application/json", + "application/yaml", + "text/yaml" + ], + "responses": { + "200": { + "description": "AlertingFileExport", + "schema": { + "$ref": "#/definitions/AlertingFileExport" + } + }, + "404": { + "description": " Not found." + } + }, + "summary": "Export an alert rule in provisioning file format.", + "tags": [ + "provisioning" + ] + } + }, "/api/v1/provisioning/contact-points": { "get": { "operationId": "RouteGetContactpoints", @@ -5938,6 +6137,52 @@ ] } }, + "/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}/export": { + "get": { + "operationId": "RouteGetAlertRuleGroupExport", + "parameters": [ + { + "in": "path", + "name": "FolderUID", + "required": true, + "type": "string" + }, + { + "in": "path", + "name": "Group", + "required": true, + "type": "string" + }, + { + "default": false, + "description": "Whether to initiate a download of the file or not.", + "in": "query", + "name": "download", + "type": "boolean" + } + ], + "produces": [ + "application/json", + "application/yaml", + "text/yaml" + ], + "responses": { + "200": { + "description": "AlertingFileExport", + "schema": { + "$ref": "#/definitions/AlertingFileExport" + } + }, + "404": { + "description": " Not found." + } + }, + "summary": "Export an alert rule group in provisioning file format.", + "tags": [ + "provisioning" + ] + } + }, "/api/v1/provisioning/mute-timings": { "get": { "operationId": "RouteGetMuteTimings", diff --git a/pkg/services/ngalert/api/tooling/spec.json b/pkg/services/ngalert/api/tooling/spec.json index ce7a76b2117..14b8d748465 100644 --- a/pkg/services/ngalert/api/tooling/spec.json +++ b/pkg/services/ngalert/api/tooling/spec.json @@ -1746,6 +1746,36 @@ } } }, + "/api/v1/provisioning/alert-rules/export": { + "get": { + "tags": [ + "provisioning", + "stable" + ], + "summary": "Export all alert rules in provisioning file format.", + "operationId": "RouteGetAlertRulesExport", + "parameters": [ + { + "type": "boolean", + "default": false, + "description": "Whether to initiate a download of the file or not.", + "name": "download", + "in": "query" + } + ], + "responses": { + "200": { + "description": "AlertingFileExport", + "schema": { + "$ref": "#/definitions/AlertingFileExport" + } + }, + "404": { + "description": " Not found." + } + } + } + }, "/api/v1/provisioning/alert-rules/{UID}": { "get": { "tags": [ @@ -1844,6 +1874,48 @@ } } }, + "/api/v1/provisioning/alert-rules/{UID}/export": { + "get": { + "produces": [ + "application/json", + "application/yaml", + "text/yaml" + ], + "tags": [ + "provisioning", + "stable" + ], + "summary": "Export an alert rule in provisioning file format.", + "operationId": "RouteGetAlertRuleExport", + "parameters": [ + { + "type": "string", + "description": "Alert rule UID", + "name": "UID", + "in": "path", + "required": true + }, + { + "type": "boolean", + "default": false, + "description": "Whether to initiate a download of the file or not.", + "name": "download", + "in": "query" + } + ], + "responses": { + "200": { + "description": "AlertingFileExport", + "schema": { + "$ref": "#/definitions/AlertingFileExport" + } + }, + "404": { + "description": " Not found." + } + } + } + }, "/api/v1/provisioning/contact-points": { "get": { "tags": [ @@ -2053,6 +2125,53 @@ } } }, + "/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}/export": { + "get": { + "produces": [ + "application/json", + "application/yaml", + "text/yaml" + ], + "tags": [ + "provisioning", + "stable" + ], + "summary": "Export an alert rule group in provisioning file format.", + "operationId": "RouteGetAlertRuleGroupExport", + "parameters": [ + { + "type": "string", + "name": "FolderUID", + "in": "path", + "required": true + }, + { + "type": "string", + "name": "Group", + "in": "path", + "required": true + }, + { + "type": "boolean", + "default": false, + "description": "Whether to initiate a download of the file or not.", + "name": "download", + "in": "query" + } + ], + "responses": { + "200": { + "description": "AlertingFileExport", + "schema": { + "$ref": "#/definitions/AlertingFileExport" + } + }, + "404": { + "description": " Not found." + } + } + } + }, "/api/v1/provisioning/mute-timings": { "get": { "tags": [ @@ -2612,6 +2731,28 @@ } } }, + "AlertQueryExport": { + "type": "object", + "title": "AlertQueryExport is the provisioned export of models.AlertQuery.", + "properties": { + "datasourceUid": { + "type": "string" + }, + "model": { + "type": "object", + "additionalProperties": {} + }, + "queryType": { + "type": "string" + }, + "refId": { + "type": "string" + }, + "relativeTimeRange": { + "$ref": "#/definitions/RelativeTimeRange" + } + } + }, "AlertResponse": { "type": "object", "required": [ @@ -2632,6 +2773,68 @@ } } }, + "AlertRuleExport": { + "type": "object", + "title": "AlertRuleExport is the provisioned file export of models.AlertRule.", + "properties": { + "annotations": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "condition": { + "type": "string" + }, + "dasboardUid": { + "type": "string" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/AlertQueryExport" + } + }, + "execErrState": { + "type": "string", + "enum": [ + "Alerting", + "Error", + "OK" + ] + }, + "for": { + "$ref": "#/definitions/Duration" + }, + "isPaused": { + "type": "boolean" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "noDataState": { + "type": "string", + "enum": [ + "Alerting", + "NoData", + "OK" + ] + }, + "panelId": { + "type": "integer", + "format": "int64" + }, + "title": { + "type": "string" + }, + "uid": { + "type": "string" + } + } + }, "AlertRuleGroup": { "type": "object", "properties": { @@ -2653,6 +2856,31 @@ } } }, + "AlertRuleGroupExport": { + "type": "object", + "title": "AlertRuleGroupExport is the provisioned file export of AlertRuleGroupV1.", + "properties": { + "folder": { + "type": "string" + }, + "interval": { + "$ref": "#/definitions/Duration" + }, + "name": { + "type": "string" + }, + "orgId": { + "type": "integer", + "format": "int64" + }, + "rules": { + "type": "array", + "items": { + "$ref": "#/definitions/AlertRuleExport" + } + } + } + }, "AlertRuleGroupMetadata": { "type": "object", "properties": { @@ -2662,6 +2890,23 @@ } } }, + "AlertingFileExport": { + "type": "object", + "title": "AlertingFileExport is the full provisioned file export.", + "properties": { + "apiVersion": { + "type": "integer", + "format": "int64" + }, + "groups": { + "type": "array", + "items": { + "$ref": "#/definitions/AlertRuleGroupExport" + } + } + }, + "$ref": "#/definitions/AlertingFileExport" + }, "AlertingRule": { "description": "adapted from cortex", "type": "object", @@ -4661,6 +4906,10 @@ "type": "integer", "format": "int64" }, + "isPaused": { + "type": "boolean", + "example": false + }, "labels": { "type": "object", "additionalProperties": { @@ -5650,8 +5899,9 @@ } }, "URL": { + "description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use RawPath, an optional field which only gets\nset if the default encoding is different from Path.\n\nURL's String method uses the EscapedPath method to obtain the path. See the\nEscapedPath method for more details.", "type": "object", - "title": "URL is a custom URL type that allows validation at configuration load time.", + "title": "A URL represents a parsed URL (technically, a URI reference).", "properties": { "ForceQuery": { "type": "boolean" @@ -5993,7 +6243,6 @@ } }, "gettableAlert": { - "description": "GettableAlert gettable alert", "type": "object", "required": [ "labels", @@ -6106,6 +6355,7 @@ "$ref": "#/definitions/gettableSilence" }, "gettableSilences": { + "description": "GettableSilences gettable silences", "type": "array", "items": { "$ref": "#/definitions/gettableSilence" @@ -6113,7 +6363,6 @@ "$ref": "#/definitions/gettableSilences" }, "integration": { - "description": "Integration integration", "type": "object", "required": [ "name", @@ -6258,6 +6507,7 @@ } }, "postableSilence": { + "description": "PostableSilence postable silence", "type": "object", "required": [ "comment", @@ -6296,7 +6546,6 @@ "$ref": "#/definitions/postableSilence" }, "receiver": { - "description": "Receiver receiver", "type": "object", "required": [ "active", diff --git a/pkg/services/ngalert/api/tooling/swagger-codegen/templates/controller-api.mustache b/pkg/services/ngalert/api/tooling/swagger-codegen/templates/controller-api.mustache index 34dfce4f1ad..7a7f7498108 100644 --- a/pkg/services/ngalert/api/tooling/swagger-codegen/templates/controller-api.mustache +++ b/pkg/services/ngalert/api/tooling/swagger-codegen/templates/controller-api.mustache @@ -11,14 +11,15 @@ import ( apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/metrics" "github.com/grafana/grafana/pkg/middleware" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" ) type {{classname}} interface { {{#operation}} - {{nickname}}(*models.ReqContext) response.Response{{/operation}} + {{nickname}}(*contextmodel.ReqContext) response.Response{{/operation}} } {{#operations}}{{#operation}} -func (f *{{classname}}Handler) {{nickname}}(ctx *models.ReqContext) response.Response { {{#hasPathParams}} +func (f *{{classname}}Handler) {{nickname}}(ctx *contextmodel.ReqContext) response.Response { {{#hasPathParams}} // Parse Path Parameters{{/hasPathParams}}{{#pathParams}} {{paramName}}Param := web.Params(ctx.Req)[":{{baseName}}"]{{/pathParams}} {{#bodyParams}} diff --git a/pkg/services/ngalert/api/util.go b/pkg/services/ngalert/api/util.go index 3a101965855..cdb5c170216 100644 --- a/pkg/services/ngalert/api/util.go +++ b/pkg/services/ngalert/api/util.go @@ -15,8 +15,8 @@ import ( "gopkg.in/yaml.v3" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasourceproxy" "github.com/grafana/grafana/pkg/services/datasources" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" @@ -34,7 +34,7 @@ func toMacaronPath(path string) string { })) } -func getDatasourceByUID(ctx *models.ReqContext, cache datasources.CacheService, expectedType apimodels.Backend) (*datasources.DataSource, error) { +func getDatasourceByUID(ctx *contextmodel.ReqContext, cache datasources.CacheService, expectedType apimodels.Backend) (*datasources.DataSource, error) { datasourceUID := web.Params(ctx.Req)[":DatasourceUID"] ds, err := cache.GetDatasourceByUID(ctx.Req.Context(), datasourceUID, ctx.SignedInUser, ctx.SkipCache) if err != nil { @@ -69,12 +69,12 @@ func (w *safeMacaronWrapper) CloseNotify() <-chan bool { // createProxyContext creates a new request context that is provided down to the data source proxy. // The request context -// 1. overwrites the underlying response writer used by a *models.ReqContext because AlertingProxy needs to intercept +// 1. overwrites the underlying response writer used by a *contextmodel.ReqContext because AlertingProxy needs to intercept // the response from the data source to analyze it and probably change // 2. elevates the current user permissions to Editor if both conditions are met: RBAC is enabled, user does not have Editor role. // This is needed to bypass the plugin authorization, which still relies on the legacy roles. // This elevation can be considered safe because all upstream calls are protected by the RBAC on web request router level. -func (p *AlertingProxy) createProxyContext(ctx *models.ReqContext, request *http.Request, response *response.NormalResponse) *models.ReqContext { +func (p *AlertingProxy) createProxyContext(ctx *contextmodel.ReqContext, request *http.Request, response *response.NormalResponse) *contextmodel.ReqContext { cpy := *ctx cpyMCtx := *cpy.Context cpyMCtx.Resp = web.NewResponseWriter(ctx.Req.Method, &safeMacaronWrapper{response}) @@ -100,7 +100,7 @@ type AlertingProxy struct { // withReq proxies a different request func (p *AlertingProxy) withReq( - ctx *models.ReqContext, + ctx *contextmodel.ReqContext, method string, u *url.URL, body io.Reader, diff --git a/pkg/services/ngalert/api/util_test.go b/pkg/services/ngalert/api/util_test.go index ce79d0fed9f..6c5d5e18980 100644 --- a/pkg/services/ngalert/api/util_test.go +++ b/pkg/services/ngalert/api/util_test.go @@ -10,9 +10,9 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/auth" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" models2 "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" @@ -41,7 +41,7 @@ func TestToMacaronPath(t *testing.T) { } func TestAlertingProxy_createProxyContext(t *testing.T) { - ctx := &models.ReqContext{ + ctx := &contextmodel.ReqContext{ Context: &web.Context{ Req: &http.Request{}, }, diff --git a/pkg/services/ngalert/eval/eval.go b/pkg/services/ngalert/eval/eval.go index 41f2a5ecddc..f4f7d6733f4 100644 --- a/pkg/services/ngalert/eval/eval.go +++ b/pkg/services/ngalert/eval/eval.go @@ -12,6 +12,9 @@ import ( "strings" "time" + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/expr" "github.com/grafana/grafana/pkg/expr/classic" "github.com/grafana/grafana/pkg/infra/log" @@ -19,9 +22,6 @@ import ( "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/setting" - - "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana-plugin-sdk-go/data" ) var logger = log.New("ngalert.eval") diff --git a/pkg/services/ngalert/eval/eval_mocks/ConditionEvaluator.go b/pkg/services/ngalert/eval/eval_mocks/ConditionEvaluator.go index 3d82a79a5e4..be2fd170f69 100644 --- a/pkg/services/ngalert/eval/eval_mocks/ConditionEvaluator.go +++ b/pkg/services/ngalert/eval/eval_mocks/ConditionEvaluator.go @@ -4,14 +4,12 @@ package eval_mocks import ( context "context" + time "time" backend "github.com/grafana/grafana-plugin-sdk-go/backend" - - eval "github.com/grafana/grafana/pkg/services/ngalert/eval" - mock "github.com/stretchr/testify/mock" - time "time" + eval "github.com/grafana/grafana/pkg/services/ngalert/eval" ) // ConditionEvaluatorMock is an autogenerated mock type for the ConditionEvaluator type diff --git a/pkg/services/ngalert/eval/extract_md.go b/pkg/services/ngalert/eval/extract_md.go index e08a7b22bbc..263e5c2a922 100644 --- a/pkg/services/ngalert/eval/extract_md.go +++ b/pkg/services/ngalert/eval/extract_md.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/expr/classic" ) diff --git a/pkg/services/ngalert/eval/extract_md_test.go b/pkg/services/ngalert/eval/extract_md_test.go index 3a1ef7849a9..6951a8caa15 100644 --- a/pkg/services/ngalert/eval/extract_md_test.go +++ b/pkg/services/ngalert/eval/extract_md_test.go @@ -4,9 +4,10 @@ import ( "testing" "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/grafana/grafana/pkg/expr/classic" "github.com/stretchr/testify/require" ptr "github.com/xorcare/pointer" + + "github.com/grafana/grafana/pkg/expr/classic" ) func TestExtractEvalString(t *testing.T) { diff --git a/pkg/services/ngalert/image/cache_mock.go b/pkg/services/ngalert/image/cache_mock.go index 655e43fe241..3ff0a3237b6 100644 --- a/pkg/services/ngalert/image/cache_mock.go +++ b/pkg/services/ngalert/image/cache_mock.go @@ -9,6 +9,7 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" + models "github.com/grafana/grafana/pkg/services/ngalert/models" ) diff --git a/pkg/services/ngalert/metrics/alertmanager.go b/pkg/services/ngalert/metrics/alertmanager.go new file mode 100644 index 00000000000..23b85e20723 --- /dev/null +++ b/pkg/services/ngalert/metrics/alertmanager.go @@ -0,0 +1,21 @@ +package metrics + +import ( + "fmt" + + "github.com/prometheus/alertmanager/api/metrics" + "github.com/prometheus/client_golang/prometheus" +) + +type Alertmanager struct { + Registerer prometheus.Registerer + *metrics.Alerts +} + +// NewAlertmanagerMetrics creates a set of metrics for the Alertmanager of each organization. +func NewAlertmanagerMetrics(r prometheus.Registerer) *Alertmanager { + return &Alertmanager{ + Registerer: r, + Alerts: metrics.NewAlerts("grafana", prometheus.WrapRegistererWithPrefix(fmt.Sprintf("%s_%s_", Namespace, Subsystem), r)), + } +} diff --git a/pkg/services/ngalert/metrics/api.go b/pkg/services/ngalert/metrics/api.go new file mode 100644 index 00000000000..db97033a0b5 --- /dev/null +++ b/pkg/services/ngalert/metrics/api.go @@ -0,0 +1,25 @@ +package metrics + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +type API struct { + RequestDuration *prometheus.HistogramVec +} + +func NewAPIMetrics(r prometheus.Registerer) *API { + return &API{ + RequestDuration: promauto.With(r).NewHistogramVec( + prometheus.HistogramOpts{ + Namespace: Namespace, + Subsystem: Subsystem, + Name: "request_duration_seconds", + Help: "Histogram of requests to the Alerting API", + Buckets: prometheus.DefBuckets, + }, + []string{"method", "route", "status_code", "backend"}, + ), + } +} diff --git a/pkg/services/ngalert/metrics/multi_org_alertmanager.go b/pkg/services/ngalert/metrics/multi_org_alertmanager.go new file mode 100644 index 00000000000..c755457755d --- /dev/null +++ b/pkg/services/ngalert/metrics/multi_org_alertmanager.go @@ -0,0 +1,32 @@ +package metrics + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +type MultiOrgAlertmanager struct { + Registerer prometheus.Registerer + ActiveConfigurations prometheus.Gauge + DiscoveredConfigurations prometheus.Gauge + registries *OrgRegistries +} + +func NewMultiOrgAlertmanagerMetrics(r prometheus.Registerer) *MultiOrgAlertmanager { + return &MultiOrgAlertmanager{ + Registerer: r, + registries: NewOrgRegistries(), + DiscoveredConfigurations: promauto.With(r).NewGauge(prometheus.GaugeOpts{ + Namespace: Namespace, + Subsystem: Subsystem, + Name: "discovered_configurations", + Help: "The number of organizations we've discovered that require an Alertmanager configuration.", + }), + ActiveConfigurations: promauto.With(r).NewGauge(prometheus.GaugeOpts{ + Namespace: Namespace, + Subsystem: Subsystem, + Name: "active_configurations", + Help: "The number of active Alertmanager configurations.", + }), + } +} diff --git a/pkg/services/ngalert/metrics/ngalert.go b/pkg/services/ngalert/metrics/ngalert.go index 035ed3a3992..ae1a304ed34 100644 --- a/pkg/services/ngalert/metrics/ngalert.go +++ b/pkg/services/ngalert/metrics/ngalert.go @@ -1,22 +1,7 @@ package metrics import ( - "fmt" - "regexp" - "strings" - "sync" - "time" - - "github.com/prometheus/alertmanager/api/metrics" "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" - - "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" - apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" - "github.com/grafana/grafana/pkg/util/ticker" - - "github.com/grafana/grafana/pkg/web" ) const ( @@ -37,47 +22,24 @@ func ProvideServiceForTest() *NGAlert { } type NGAlert struct { - // Registerer is for use by subcomponents which register their own metrics. - Registerer prometheus.Registerer + // Registerer is used by subcomponents which register their own metrics. + Registerer prometheus.Registerer + schedulerMetrics *Scheduler stateMetrics *State multiOrgAlertmanagerMetrics *MultiOrgAlertmanager apiMetrics *API } -type Scheduler struct { - Registerer prometheus.Registerer - BehindSeconds prometheus.Gauge - EvalTotal *prometheus.CounterVec - EvalFailures *prometheus.CounterVec - EvalDuration *prometheus.HistogramVec - SchedulePeriodicDuration prometheus.Histogram - SchedulableAlertRules prometheus.Gauge - SchedulableAlertRulesHash prometheus.Gauge - UpdateSchedulableAlertRulesDuration prometheus.Histogram - Ticker *ticker.Metrics - EvaluationMissed *prometheus.CounterVec -} - -type MultiOrgAlertmanager struct { - Registerer prometheus.Registerer - ActiveConfigurations prometheus.Gauge - DiscoveredConfigurations prometheus.Gauge - registries *OrgRegistries -} - -type API struct { - RequestDuration *prometheus.HistogramVec -} - -type Alertmanager struct { - Registerer prometheus.Registerer - *metrics.Alerts -} - -type State struct { - GroupRules *prometheus.GaugeVec - AlertState *prometheus.GaugeVec +// NewNGAlert manages the metrics of all the alerting components. +func NewNGAlert(r prometheus.Registerer) *NGAlert { + return &NGAlert{ + Registerer: r, + schedulerMetrics: NewSchedulerMetrics(r), + stateMetrics: NewStateMetrics(r), + multiOrgAlertmanagerMetrics: NewMultiOrgAlertmanagerMetrics(r), + apiMetrics: NewAPIMetrics(r), + } } func (ng *NGAlert) GetSchedulerMetrics() *Scheduler { @@ -96,25 +58,6 @@ func (ng *NGAlert) GetMultiOrgAlertmanagerMetrics() *MultiOrgAlertmanager { return ng.multiOrgAlertmanagerMetrics } -// NewNGAlert manages the metrics of all the alerting components. -func NewNGAlert(r prometheus.Registerer) *NGAlert { - return &NGAlert{ - Registerer: r, - schedulerMetrics: NewSchedulerMetrics(r), - stateMetrics: newStateMetrics(r), - multiOrgAlertmanagerMetrics: newMultiOrgAlertmanagerMetrics(r), - apiMetrics: newAPIMetrics(r), - } -} - -// NewAlertmanagerMetrics creates a set of metrics for the Alertmanager of each organization. -func NewAlertmanagerMetrics(r prometheus.Registerer) *Alertmanager { - return &Alertmanager{ - Registerer: r, - Alerts: metrics.NewAlerts("grafana", prometheus.WrapRegistererWithPrefix(fmt.Sprintf("%s_%s_", Namespace, Subsystem), r)), - } -} - // RemoveOrgRegistry removes the *prometheus.Registry for the specified org. It is safe to call concurrently. func (moa *MultiOrgAlertmanager) RemoveOrgRegistry(id int64) { moa.registries.RemoveOrgRegistry(id) @@ -124,232 +67,3 @@ func (moa *MultiOrgAlertmanager) RemoveOrgRegistry(id int64) { func (moa *MultiOrgAlertmanager) GetOrCreateOrgRegistry(id int64) prometheus.Registerer { return moa.registries.GetOrCreateOrgRegistry(id) } - -func NewSchedulerMetrics(r prometheus.Registerer) *Scheduler { - return &Scheduler{ - Registerer: r, - BehindSeconds: promauto.With(r).NewGauge(prometheus.GaugeOpts{ - Namespace: Namespace, - Subsystem: Subsystem, - Name: "scheduler_behind_seconds", - Help: "The total number of seconds the scheduler is behind.", - }), - // TODO: once rule groups support multiple rules, consider partitioning - // on rule group as well as tenant, similar to loki|cortex. - EvalTotal: promauto.With(r).NewCounterVec( - prometheus.CounterOpts{ - Namespace: Namespace, - Subsystem: Subsystem, - Name: "rule_evaluations_total", - Help: "The total number of rule evaluations.", - }, - []string{"org"}, - ), - // TODO: once rule groups support multiple rules, consider partitioning - // on rule group as well as tenant, similar to loki|cortex. - EvalFailures: promauto.With(r).NewCounterVec( - prometheus.CounterOpts{ - Namespace: Namespace, - Subsystem: Subsystem, - Name: "rule_evaluation_failures_total", - Help: "The total number of rule evaluation failures.", - }, - []string{"org"}, - ), - EvalDuration: promauto.With(r).NewHistogramVec( - prometheus.HistogramOpts{ - Namespace: Namespace, - Subsystem: Subsystem, - Name: "rule_evaluation_duration_seconds", - Help: "The duration for a rule to execute.", - Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10, 25, 50, 100}, - }, - []string{"org"}, - ), - SchedulePeriodicDuration: promauto.With(r).NewHistogram( - prometheus.HistogramOpts{ - Namespace: Namespace, - Subsystem: Subsystem, - Name: "schedule_periodic_duration_seconds", - Help: "The time taken to run the scheduler.", - Buckets: []float64{0.1, 0.25, 0.5, 1, 2, 5, 10}, - }, - ), - SchedulableAlertRules: promauto.With(r).NewGauge( - prometheus.GaugeOpts{ - Namespace: Namespace, - Subsystem: Subsystem, - Name: "schedule_alert_rules", - Help: "The number of alert rules that could be considered for evaluation at the next tick.", - }, - ), - SchedulableAlertRulesHash: promauto.With(r).NewGauge( - prometheus.GaugeOpts{ - Namespace: Namespace, - Subsystem: Subsystem, - Name: "schedule_alert_rules_hash", - Help: "A hash of the alert rules that could be considered for evaluation at the next tick.", - }), - UpdateSchedulableAlertRulesDuration: promauto.With(r).NewHistogram( - prometheus.HistogramOpts{ - Namespace: Namespace, - Subsystem: Subsystem, - Name: "schedule_query_alert_rules_duration_seconds", - Help: "The time taken to fetch alert rules from the database.", - Buckets: []float64{0.1, 0.25, 0.5, 1, 2, 5, 10}, - }, - ), - Ticker: ticker.NewMetrics(r, "alerting"), - EvaluationMissed: promauto.With(r).NewCounterVec( - prometheus.CounterOpts{ - Namespace: Namespace, - Subsystem: Subsystem, - Name: "schedule_rule_evaluations_missed_total", - Help: "The total number of rule evaluations missed due to a slow rule evaluation.", - }, - []string{"org", "name"}, - ), - } -} - -func newStateMetrics(r prometheus.Registerer) *State { - return &State{ - // TODO: once rule groups support multiple rules, consider partitioning - // on rule group as well as tenant, similar to loki|cortex. - GroupRules: promauto.With(r).NewGaugeVec( - prometheus.GaugeOpts{ - Namespace: Namespace, - Subsystem: Subsystem, - Name: "rule_group_rules", - Help: "The number of rules.", - }, - []string{"org"}, - ), - AlertState: promauto.With(r).NewGaugeVec(prometheus.GaugeOpts{ - Namespace: Namespace, - Subsystem: Subsystem, - Name: "alerts", - Help: "How many alerts by state.", - }, []string{"state"}), - } -} - -func newMultiOrgAlertmanagerMetrics(r prometheus.Registerer) *MultiOrgAlertmanager { - return &MultiOrgAlertmanager{ - Registerer: r, - registries: NewOrgRegistries(), - DiscoveredConfigurations: promauto.With(r).NewGauge(prometheus.GaugeOpts{ - Namespace: Namespace, - Subsystem: Subsystem, - Name: "discovered_configurations", - Help: "The number of organizations we've discovered that require an Alertmanager configuration.", - }), - ActiveConfigurations: promauto.With(r).NewGauge(prometheus.GaugeOpts{ - Namespace: Namespace, - Subsystem: Subsystem, - Name: "active_configurations", - Help: "The number of active Alertmanager configurations.", - }), - } -} - -func newAPIMetrics(r prometheus.Registerer) *API { - return &API{ - RequestDuration: promauto.With(r).NewHistogramVec( - prometheus.HistogramOpts{ - Namespace: Namespace, - Subsystem: Subsystem, - Name: "request_duration_seconds", - Help: "Histogram of requests to the Alerting API", - Buckets: prometheus.DefBuckets, - }, - []string{"method", "route", "status_code", "backend"}, - ), - } -} - -// OrgRegistries represents a map of registries per org. -type OrgRegistries struct { - regsMu sync.Mutex - regs map[int64]prometheus.Registerer -} - -func NewOrgRegistries() *OrgRegistries { - return &OrgRegistries{ - regs: make(map[int64]prometheus.Registerer), - } -} - -// GetOrCreateOrgRegistry gets or creates a *prometheus.Registry for the specified org. It is safe to call concurrently. -func (m *OrgRegistries) GetOrCreateOrgRegistry(orgID int64) prometheus.Registerer { - m.regsMu.Lock() - defer m.regsMu.Unlock() - - orgRegistry, ok := m.regs[orgID] - if !ok { - reg := prometheus.NewRegistry() - m.regs[orgID] = reg - return reg - } - return orgRegistry -} - -// RemoveOrgRegistry removes the *prometheus.Registry for the specified org. It is safe to call concurrently. -func (m *OrgRegistries) RemoveOrgRegistry(org int64) { - m.regsMu.Lock() - defer m.regsMu.Unlock() - delete(m.regs, org) -} - -// Instrument wraps a middleware, instrumenting the request latencies. -func Instrument( - method, - path string, - action func(*models.ReqContext) response.Response, - metrics *API, -) web.Handler { - normalizedPath := MakeLabelValue(path) - - return func(c *models.ReqContext) { - start := time.Now() - res := action(c) - - // TODO: We could look up the datasource type via our datasource service - var backend string - datasourceID := web.Params(c.Req)[":DatasourceID"] - if datasourceID == apimodels.GrafanaBackend.String() || datasourceID == "" { - backend = GrafanaBackend - } else { - backend = ProxyBackend - } - - ls := prometheus.Labels{ - "method": method, - "route": normalizedPath, - "status_code": fmt.Sprint(res.Status()), - "backend": backend, - } - res.WriteTo(c) - metrics.RequestDuration.With(ls).Observe(time.Since(start).Seconds()) - } -} - -var invalidChars = regexp.MustCompile(`[^a-zA-Z0-9]+`) - -// MakeLabelValue normalizes a path template -func MakeLabelValue(path string) string { - // Convert non-alnums to underscores. - result := invalidChars.ReplaceAllString(path, "_") - - // Trim leading and trailing underscores. - result = strings.Trim(result, "_") - - // Make it all lowercase - result = strings.ToLower(result) - - // Special case. - if result == "" { - result = "root" - } - return result -} diff --git a/pkg/services/ngalert/metrics/scheduler.go b/pkg/services/ngalert/metrics/scheduler.go new file mode 100644 index 00000000000..4cd844b1bef --- /dev/null +++ b/pkg/services/ngalert/metrics/scheduler.go @@ -0,0 +1,109 @@ +package metrics + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + + "github.com/grafana/grafana/pkg/util/ticker" +) + +type Scheduler struct { + Registerer prometheus.Registerer + BehindSeconds prometheus.Gauge + EvalTotal *prometheus.CounterVec + EvalFailures *prometheus.CounterVec + EvalDuration *prometheus.HistogramVec + SchedulePeriodicDuration prometheus.Histogram + SchedulableAlertRules prometheus.Gauge + SchedulableAlertRulesHash prometheus.Gauge + UpdateSchedulableAlertRulesDuration prometheus.Histogram + Ticker *ticker.Metrics + EvaluationMissed *prometheus.CounterVec +} + +func NewSchedulerMetrics(r prometheus.Registerer) *Scheduler { + return &Scheduler{ + Registerer: r, + BehindSeconds: promauto.With(r).NewGauge(prometheus.GaugeOpts{ + Namespace: Namespace, + Subsystem: Subsystem, + Name: "scheduler_behind_seconds", + Help: "The total number of seconds the scheduler is behind.", + }), + // TODO: once rule groups support multiple rules, consider partitioning + // on rule group as well as tenant, similar to loki|cortex. + EvalTotal: promauto.With(r).NewCounterVec( + prometheus.CounterOpts{ + Namespace: Namespace, + Subsystem: Subsystem, + Name: "rule_evaluations_total", + Help: "The total number of rule evaluations.", + }, + []string{"org"}, + ), + // TODO: once rule groups support multiple rules, consider partitioning + // on rule group as well as tenant, similar to loki|cortex. + EvalFailures: promauto.With(r).NewCounterVec( + prometheus.CounterOpts{ + Namespace: Namespace, + Subsystem: Subsystem, + Name: "rule_evaluation_failures_total", + Help: "The total number of rule evaluation failures.", + }, + []string{"org"}, + ), + EvalDuration: promauto.With(r).NewHistogramVec( + prometheus.HistogramOpts{ + Namespace: Namespace, + Subsystem: Subsystem, + Name: "rule_evaluation_duration_seconds", + Help: "The duration for a rule to execute.", + Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10, 25, 50, 100}, + }, + []string{"org"}, + ), + SchedulePeriodicDuration: promauto.With(r).NewHistogram( + prometheus.HistogramOpts{ + Namespace: Namespace, + Subsystem: Subsystem, + Name: "schedule_periodic_duration_seconds", + Help: "The time taken to run the scheduler.", + Buckets: []float64{0.1, 0.25, 0.5, 1, 2, 5, 10}, + }, + ), + SchedulableAlertRules: promauto.With(r).NewGauge( + prometheus.GaugeOpts{ + Namespace: Namespace, + Subsystem: Subsystem, + Name: "schedule_alert_rules", + Help: "The number of alert rules that could be considered for evaluation at the next tick.", + }, + ), + SchedulableAlertRulesHash: promauto.With(r).NewGauge( + prometheus.GaugeOpts{ + Namespace: Namespace, + Subsystem: Subsystem, + Name: "schedule_alert_rules_hash", + Help: "A hash of the alert rules that could be considered for evaluation at the next tick.", + }), + UpdateSchedulableAlertRulesDuration: promauto.With(r).NewHistogram( + prometheus.HistogramOpts{ + Namespace: Namespace, + Subsystem: Subsystem, + Name: "schedule_query_alert_rules_duration_seconds", + Help: "The time taken to fetch alert rules from the database.", + Buckets: []float64{0.1, 0.25, 0.5, 1, 2, 5, 10}, + }, + ), + Ticker: ticker.NewMetrics(r, "alerting"), + EvaluationMissed: promauto.With(r).NewCounterVec( + prometheus.CounterOpts{ + Namespace: Namespace, + Subsystem: Subsystem, + Name: "schedule_rule_evaluations_missed_total", + Help: "The total number of rule evaluations missed due to a slow rule evaluation.", + }, + []string{"org", "name"}, + ), + } +} diff --git a/pkg/services/ngalert/metrics/state.go b/pkg/services/ngalert/metrics/state.go new file mode 100644 index 00000000000..2a5bf072b1a --- /dev/null +++ b/pkg/services/ngalert/metrics/state.go @@ -0,0 +1,33 @@ +package metrics + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +type State struct { + GroupRules *prometheus.GaugeVec + AlertState *prometheus.GaugeVec +} + +func NewStateMetrics(r prometheus.Registerer) *State { + return &State{ + // TODO: once rule groups support multiple rules, consider partitioning + // on rule group as well as tenant, similar to loki|cortex. + GroupRules: promauto.With(r).NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: Namespace, + Subsystem: Subsystem, + Name: "rule_group_rules", + Help: "The number of rules.", + }, + []string{"org"}, + ), + AlertState: promauto.With(r).NewGaugeVec(prometheus.GaugeOpts{ + Namespace: Namespace, + Subsystem: Subsystem, + Name: "alerts", + Help: "How many alerts by state.", + }, []string{"state"}), + } +} diff --git a/pkg/services/ngalert/metrics/util.go b/pkg/services/ngalert/metrics/util.go new file mode 100644 index 00000000000..eaa82e9c8ae --- /dev/null +++ b/pkg/services/ngalert/metrics/util.go @@ -0,0 +1,102 @@ +package metrics + +import ( + "fmt" + "regexp" + "strings" + "sync" + "time" + + "github.com/prometheus/client_golang/prometheus" + + "github.com/grafana/grafana/pkg/api/response" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" + apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + "github.com/grafana/grafana/pkg/web" +) + +// OrgRegistries represents a map of registries per org. +type OrgRegistries struct { + regsMu sync.Mutex + regs map[int64]prometheus.Registerer +} + +func NewOrgRegistries() *OrgRegistries { + return &OrgRegistries{ + regs: make(map[int64]prometheus.Registerer), + } +} + +// GetOrCreateOrgRegistry gets or creates a *prometheus.Registry for the specified org. It is safe to call concurrently. +func (m *OrgRegistries) GetOrCreateOrgRegistry(orgID int64) prometheus.Registerer { + m.regsMu.Lock() + defer m.regsMu.Unlock() + + orgRegistry, ok := m.regs[orgID] + if !ok { + reg := prometheus.NewRegistry() + m.regs[orgID] = reg + return reg + } + return orgRegistry +} + +// RemoveOrgRegistry removes the *prometheus.Registry for the specified org. It is safe to call concurrently. +func (m *OrgRegistries) RemoveOrgRegistry(org int64) { + m.regsMu.Lock() + defer m.regsMu.Unlock() + delete(m.regs, org) +} + +// Instrument wraps a middleware, instrumenting the request latencies. +func Instrument( + method, + path string, + action func(*contextmodel.ReqContext) response.Response, + metrics *API, +) web.Handler { + normalizedPath := MakeLabelValue(path) + + return func(c *contextmodel.ReqContext) { + start := time.Now() + res := action(c) + + // TODO: We could look up the datasource type via our datasource service + var backend string + datasourceID := web.Params(c.Req)[":DatasourceID"] + if datasourceID == apimodels.GrafanaBackend.String() || datasourceID == "" { + backend = GrafanaBackend + } else { + backend = ProxyBackend + } + + ls := prometheus.Labels{ + "method": method, + "route": normalizedPath, + "status_code": fmt.Sprint(res.Status()), + "backend": backend, + } + res.WriteTo(c) + metrics.RequestDuration.With(ls).Observe(time.Since(start).Seconds()) + } +} + +var invalidChars = regexp.MustCompile(`[^a-zA-Z0-9]+`) + +// MakeLabelValue normalizes a path template +func MakeLabelValue(path string) string { + // Convert non-alnums to underscores. + result := invalidChars.ReplaceAllString(path, "_") + + // Trim leading and trailing underscores. + result = strings.Trim(result, "_") + + // Make it all lowercase + result = strings.ToLower(result) + + // Special case. + if result == "" { + result = "root" + } + return result +} diff --git a/pkg/services/ngalert/models/alert_rule.go b/pkg/services/ngalert/models/alert_rule.go index 0a0cbb4607c..5e96c82cbca 100644 --- a/pkg/services/ngalert/models/alert_rule.go +++ b/pkg/services/ngalert/models/alert_rule.go @@ -11,8 +11,8 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" - alertingModels "github.com/grafana/alerting/alerting/models" + "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/util/cmputil" ) @@ -111,6 +111,7 @@ const ( StateReasonError = "Error" StateReasonPaused = "Paused" StateReasonUpdated = "Updated" + StateReasonRuleDeleted = "RuleDeleted" ) var ( diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index cc92ef97891..3908c308cd0 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -239,7 +239,7 @@ func (ng *AlertNG) init() error { contactPointService := provisioning.NewContactPointService(store, ng.SecretsService, store, store, ng.Log) templateService := provisioning.NewTemplateService(store, store, store, ng.Log) muteTimingService := provisioning.NewMuteTimingService(store, store, store, ng.Log) - alertRuleService := provisioning.NewAlertRuleService(store, store, ng.QuotaService, store, + alertRuleService := provisioning.NewAlertRuleService(store, store, ng.dashboardService, ng.QuotaService, store, int64(ng.Cfg.UnifiedAlerting.DefaultRuleEvaluationInterval.Seconds()), int64(ng.Cfg.UnifiedAlerting.BaseInterval.Seconds()), ng.Log) diff --git a/pkg/services/ngalert/notifier/alertmanager.go b/pkg/services/ngalert/notifier/alertmanager.go index 05b83bfc284..f6aca475f63 100644 --- a/pkg/services/ngalert/notifier/alertmanager.go +++ b/pkg/services/ngalert/notifier/alertmanager.go @@ -13,6 +13,8 @@ import ( "github.com/grafana/alerting/alerting" "github.com/grafana/alerting/alerting/notifier/channels" + amv2 "github.com/prometheus/alertmanager/api/v2/models" + "github.com/grafana/grafana/pkg/infra/kvstore" "github.com/grafana/grafana/pkg/infra/log" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" @@ -22,8 +24,6 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/setting" - - amv2 "github.com/prometheus/alertmanager/api/v2/models" ) const ( diff --git a/pkg/services/ngalert/notifier/alertmanager_test.go b/pkg/services/ngalert/notifier/alertmanager_test.go index 889de070c0c..b2a0408745f 100644 --- a/pkg/services/ngalert/notifier/alertmanager_test.go +++ b/pkg/services/ngalert/notifier/alertmanager_test.go @@ -5,16 +5,15 @@ import ( "testing" "time" - "github.com/grafana/grafana/pkg/services/dashboards" - "github.com/grafana/grafana/pkg/services/secrets/database" - "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/ngalert/metrics" "github.com/grafana/grafana/pkg/services/ngalert/store" + "github.com/grafana/grafana/pkg/services/secrets/database" secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager" "github.com/grafana/grafana/pkg/setting" ) diff --git a/pkg/services/ngalert/notifier/config_test.go b/pkg/services/ngalert/notifier/config_test.go index df04d6fc4b3..5650ffb3471 100644 --- a/pkg/services/ngalert/notifier/config_test.go +++ b/pkg/services/ngalert/notifier/config_test.go @@ -6,9 +6,10 @@ import ( "path/filepath" "testing" - api "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + api "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" ) func TestPersistTemplates(t *testing.T) { diff --git a/pkg/services/ngalert/notifier/multiorg_alertmanager.go b/pkg/services/ngalert/notifier/multiorg_alertmanager.go index 785f5779d02..7757c0220b0 100644 --- a/pkg/services/ngalert/notifier/multiorg_alertmanager.go +++ b/pkg/services/ngalert/notifier/multiorg_alertmanager.go @@ -9,6 +9,11 @@ import ( "sync" "time" + "github.com/grafana/alerting/alerting" + "github.com/grafana/alerting/alerting/notifier/channels" + "github.com/prometheus/alertmanager/cluster" + "github.com/prometheus/client_golang/prometheus" + "github.com/grafana/grafana/pkg/infra/kvstore" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/ngalert/metrics" @@ -18,11 +23,6 @@ import ( "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/setting" - - "github.com/grafana/alerting/alerting" - "github.com/grafana/alerting/alerting/notifier/channels" - "github.com/prometheus/alertmanager/cluster" - "github.com/prometheus/client_golang/prometheus" ) var ( diff --git a/pkg/services/ngalert/notifier/multiorg_alertmanager_test.go b/pkg/services/ngalert/notifier/multiorg_alertmanager_test.go index 4011dd31cfe..521e980ea00 100644 --- a/pkg/services/ngalert/notifier/multiorg_alertmanager_test.go +++ b/pkg/services/ngalert/notifier/multiorg_alertmanager_test.go @@ -10,6 +10,10 @@ import ( "testing" "time" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/testutil" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/ngalert/metrics" "github.com/grafana/grafana/pkg/services/ngalert/models" @@ -17,10 +21,6 @@ import ( "github.com/grafana/grafana/pkg/services/secrets/fakes" secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager" "github.com/grafana/grafana/pkg/setting" - - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/testutil" - "github.com/stretchr/testify/require" ) func TestMultiOrgAlertmanager_SyncAlertmanagersForOrgs(t *testing.T) { diff --git a/pkg/services/ngalert/notifier/receivers.go b/pkg/services/ngalert/notifier/receivers.go index 2a964b1e5d0..7e56baaa64b 100644 --- a/pkg/services/ngalert/notifier/receivers.go +++ b/pkg/services/ngalert/notifier/receivers.go @@ -7,12 +7,12 @@ import ( "fmt" "time" - "github.com/grafana/alerting/alerting" - "github.com/go-openapi/strfmt" - apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + "github.com/grafana/alerting/alerting" "github.com/prometheus/alertmanager/api/v2/models" "github.com/prometheus/alertmanager/types" + + apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" ) var ( diff --git a/pkg/services/ngalert/notifier/receivers_test.go b/pkg/services/ngalert/notifier/receivers_test.go index 25d3195b2af..d9b923c5de9 100644 --- a/pkg/services/ngalert/notifier/receivers_test.go +++ b/pkg/services/ngalert/notifier/receivers_test.go @@ -7,7 +7,6 @@ import ( "testing" "github.com/grafana/alerting/alerting" - "github.com/stretchr/testify/require" ) diff --git a/pkg/services/ngalert/provisioning/alert_rules.go b/pkg/services/ngalert/provisioning/alert_rules.go index 27d6b531946..18b2ba3f361 100644 --- a/pkg/services/ngalert/provisioning/alert_rules.go +++ b/pkg/services/ngalert/provisioning/alert_rules.go @@ -4,11 +4,14 @@ import ( "context" "errors" "fmt" + "sort" "time" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/store" + "github.com/grafana/grafana/pkg/services/provisioning/alerting/file" "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/util" ) @@ -18,6 +21,7 @@ type AlertRuleService struct { baseIntervalSeconds int64 ruleStore RuleStore provenanceStore ProvisioningStore + dashboardService dashboards.DashboardService quotas QuotaChecker xact TransactionManager log log.Logger @@ -25,6 +29,7 @@ type AlertRuleService struct { func NewAlertRuleService(ruleStore RuleStore, provenanceStore ProvisioningStore, + dashboardService dashboards.DashboardService, quotas QuotaChecker, xact TransactionManager, defaultIntervalSeconds int64, @@ -35,6 +40,7 @@ func NewAlertRuleService(ruleStore RuleStore, baseIntervalSeconds: baseIntervalSeconds, ruleStore: ruleStore, provenanceStore: provenanceStore, + dashboardService: dashboardService, quotas: quotas, xact: xact, log: log, @@ -69,6 +75,38 @@ func (service *AlertRuleService) GetAlertRule(ctx context.Context, orgID int64, return *query.Result, provenance, nil } +type AlertRuleWithFolderTitle struct { + AlertRule models.AlertRule + FolderTitle string +} + +// GetAlertRuleWithFolderTitle returns a single alert rule with its folder title. +func (service *AlertRuleService) GetAlertRuleWithFolderTitle(ctx context.Context, orgID int64, ruleUID string) (AlertRuleWithFolderTitle, error) { + query := &models.GetAlertRuleByUIDQuery{ + OrgID: orgID, + UID: ruleUID, + } + err := service.ruleStore.GetAlertRuleByUID(ctx, query) + if err != nil { + return AlertRuleWithFolderTitle{}, err + } + + dq := dashboards.GetDashboardQuery{ + OrgID: orgID, + UID: query.Result.NamespaceUID, + } + + dash, err := service.dashboardService.GetDashboard(ctx, &dq) + if err != nil { + return AlertRuleWithFolderTitle{}, err + } + + return AlertRuleWithFolderTitle{ + AlertRule: *query.Result, + FolderTitle: dash.Title, + }, nil +} + // CreateAlertRule creates a new alert rule. This function will ignore any // interval that is set in the rule struct and use the already existing group // interval or the default one. @@ -114,10 +152,10 @@ func (service *AlertRuleService) CreateAlertRule(ctx context.Context, rule model return rule, nil } -func (service *AlertRuleService) GetRuleGroup(ctx context.Context, orgID int64, folder, group string) (models.AlertRuleGroup, error) { +func (service *AlertRuleService) GetRuleGroup(ctx context.Context, orgID int64, namespaceUID, group string) (models.AlertRuleGroup, error) { q := models.ListAlertRulesQuery{ OrgID: orgID, - NamespaceUIDs: []string{folder}, + NamespaceUIDs: []string{namespaceUID}, RuleGroup: group, } if err := service.ruleStore.ListAlertRules(ctx, &q); err != nil { @@ -366,6 +404,114 @@ func (service *AlertRuleService) deleteRules(ctx context.Context, orgID int64, t return nil } +// GetAlertRuleGroupWithFolderTitle returns the alert rule group with folder title. +func (service *AlertRuleService) GetAlertRuleGroupWithFolderTitle(ctx context.Context, orgID int64, namespaceUID, group string) (file.AlertRuleGroupWithFolderTitle, error) { + q := models.ListAlertRulesQuery{ + OrgID: orgID, + NamespaceUIDs: []string{namespaceUID}, + RuleGroup: group, + } + if err := service.ruleStore.ListAlertRules(ctx, &q); err != nil { + return file.AlertRuleGroupWithFolderTitle{}, err + } + if len(q.Result) == 0 { + return file.AlertRuleGroupWithFolderTitle{}, store.ErrAlertRuleGroupNotFound + } + + dq := dashboards.GetDashboardQuery{ + OrgID: orgID, + UID: namespaceUID, + } + dash, err := service.dashboardService.GetDashboard(ctx, &dq) + if err != nil { + return file.AlertRuleGroupWithFolderTitle{}, err + } + + res := file.AlertRuleGroupWithFolderTitle{ + AlertRuleGroup: &models.AlertRuleGroup{ + Title: q.Result[0].RuleGroup, + FolderUID: q.Result[0].NamespaceUID, + Interval: q.Result[0].IntervalSeconds, + Rules: []models.AlertRule{}, + }, + OrgID: orgID, + FolderTitle: dash.Title, + } + for _, r := range q.Result { + if r != nil { + res.AlertRuleGroup.Rules = append(res.AlertRuleGroup.Rules, *r) + } + } + return res, nil +} + +// GetAlertGroupsWithFolderTitle returns all groups with folder title that have at least one alert. +func (service *AlertRuleService) GetAlertGroupsWithFolderTitle(ctx context.Context, orgID int64) ([]file.AlertRuleGroupWithFolderTitle, error) { + q := models.ListAlertRulesQuery{ + OrgID: orgID, + } + + if err := service.ruleStore.ListAlertRules(ctx, &q); err != nil { + return nil, err + } + + groups := make(map[models.AlertRuleGroupKey][]models.AlertRule) + namespaces := make(map[string][]*models.AlertRuleGroupKey) + for _, r := range q.Result { + groupKey := r.GetGroupKey() + group := groups[groupKey] + group = append(group, *r) + groups[groupKey] = group + + namespaces[r.NamespaceUID] = append(namespaces[r.NamespaceUID], &groupKey) + } + + dq := dashboards.GetDashboardsQuery{ + DashboardUIDs: nil, + } + for uid := range namespaces { + dq.DashboardUIDs = append(dq.DashboardUIDs, uid) + } + + // We need folder titles for the provisioning file format. We do it this way instead of using GetUserVisibleNamespaces to avoid folder:read permissions that should not apply to those with alert.provisioning:read. + dashes, err := service.dashboardService.GetDashboards(ctx, &dq) + if err != nil { + return nil, err + } + folderUidToTitle := make(map[string]string) + for _, dash := range dashes { + folderUidToTitle[dash.UID] = dash.Title + } + + result := make([]file.AlertRuleGroupWithFolderTitle, 0) + for groupKey, rules := range groups { + title, ok := folderUidToTitle[groupKey.NamespaceUID] + if !ok { + return nil, fmt.Errorf("cannot find title for folder with uid '%s'", groupKey.NamespaceUID) + } + result = append(result, file.AlertRuleGroupWithFolderTitle{ + AlertRuleGroup: &models.AlertRuleGroup{ + Title: rules[0].RuleGroup, + FolderUID: rules[0].NamespaceUID, + Interval: rules[0].IntervalSeconds, + Rules: rules, + }, + OrgID: orgID, + FolderTitle: title, + }) + } + + // Return results in a stable manner. + sort.SliceStable(result, func(i, j int) bool { + if result[i].AlertRuleGroup.FolderUID == result[j].AlertRuleGroup.FolderUID { + return result[i].AlertRuleGroup.Title < result[j].AlertRuleGroup.Title + } + return result[i].AlertRuleGroup.FolderUID < result[j].AlertRuleGroup.FolderUID + }) + + return result, nil +} + // syncRuleGroupFields synchronizes calculated fields across multiple rules in a group. func syncGroupRuleFields(group *models.AlertRuleGroup, orgID int64) *models.AlertRuleGroup { for i := range group.Rules { diff --git a/pkg/services/ngalert/provisioning/alert_rules_test.go b/pkg/services/ngalert/provisioning/alert_rules_test.go index 43c37a2efbd..993930887b3 100644 --- a/pkg/services/ngalert/provisioning/alert_rules_test.go +++ b/pkg/services/ngalert/provisioning/alert_rules_test.go @@ -7,13 +7,13 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/setting" - - "github.com/stretchr/testify/require" ) func TestAlertRuleService(t *testing.T) { diff --git a/pkg/services/ngalert/provisioning/mute_timings.go b/pkg/services/ngalert/provisioning/mute_timings.go index a96e55f7c22..ef65d70dffe 100644 --- a/pkg/services/ngalert/provisioning/mute_timings.go +++ b/pkg/services/ngalert/provisioning/mute_timings.go @@ -4,10 +4,11 @@ import ( "context" "fmt" + "github.com/prometheus/alertmanager/config" + "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" - "github.com/prometheus/alertmanager/config" ) type MuteTimingService struct { diff --git a/pkg/services/ngalert/provisioning/mute_timings_test.go b/pkg/services/ngalert/provisioning/mute_timings_test.go index 84845a9ba53..3c1f2a09d70 100644 --- a/pkg/services/ngalert/provisioning/mute_timings_test.go +++ b/pkg/services/ngalert/provisioning/mute_timings_test.go @@ -5,12 +5,13 @@ import ( "fmt" "testing" - "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" "github.com/prometheus/alertmanager/config" mock "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + + "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" ) func TestMuteTimingService(t *testing.T) { diff --git a/pkg/services/ngalert/provisioning/notification_policies_test.go b/pkg/services/ngalert/provisioning/notification_policies_test.go index 11e551a20e2..5ffd39c06d9 100644 --- a/pkg/services/ngalert/provisioning/notification_policies_test.go +++ b/pkg/services/ngalert/provisioning/notification_policies_test.go @@ -4,15 +4,16 @@ import ( "context" "testing" - "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" - "github.com/grafana/grafana/pkg/setting" "github.com/prometheus/alertmanager/config" "github.com/prometheus/alertmanager/timeinterval" "github.com/prometheus/common/model" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + + "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" + "github.com/grafana/grafana/pkg/setting" ) func TestNotificationPolicyService(t *testing.T) { diff --git a/pkg/services/ngalert/provisioning/persist_mock.go b/pkg/services/ngalert/provisioning/persist_mock.go index 7a0c1c1d0be..61726e40cec 100644 --- a/pkg/services/ngalert/provisioning/persist_mock.go +++ b/pkg/services/ngalert/provisioning/persist_mock.go @@ -4,11 +4,11 @@ package provisioning import ( context "context" + testing "testing" - models "github.com/grafana/grafana/pkg/services/ngalert/models" mock "github.com/stretchr/testify/mock" - testing "testing" + models "github.com/grafana/grafana/pkg/services/ngalert/models" ) // MockAMConfigStore is an autogenerated mock type for the AMConfigStore type diff --git a/pkg/services/ngalert/provisioning/provisioning_store_mock.go b/pkg/services/ngalert/provisioning/provisioning_store_mock.go index ebc65ef8857..da168e92ba4 100644 --- a/pkg/services/ngalert/provisioning/provisioning_store_mock.go +++ b/pkg/services/ngalert/provisioning/provisioning_store_mock.go @@ -4,11 +4,11 @@ package provisioning import ( context "context" + testing "testing" - models "github.com/grafana/grafana/pkg/services/ngalert/models" mock "github.com/stretchr/testify/mock" - testing "testing" + models "github.com/grafana/grafana/pkg/services/ngalert/models" ) // MockProvisioningStore is an autogenerated mock type for the ProvisioningStore type diff --git a/pkg/services/ngalert/provisioning/quota_checker_mock.go b/pkg/services/ngalert/provisioning/quota_checker_mock.go index fcdfbfdbaa0..8261378ee37 100644 --- a/pkg/services/ngalert/provisioning/quota_checker_mock.go +++ b/pkg/services/ngalert/provisioning/quota_checker_mock.go @@ -5,8 +5,9 @@ package provisioning import ( context "context" - quota "github.com/grafana/grafana/pkg/services/quota" mock "github.com/stretchr/testify/mock" + + quota "github.com/grafana/grafana/pkg/services/quota" ) // MockQuotaChecker is an autogenerated mock type for the QuotaChecker type diff --git a/pkg/services/ngalert/provisioning/templates_test.go b/pkg/services/ngalert/provisioning/templates_test.go index c9fa25785a1..49d01d8e221 100644 --- a/pkg/services/ngalert/provisioning/templates_test.go +++ b/pkg/services/ngalert/provisioning/templates_test.go @@ -5,12 +5,13 @@ import ( "fmt" "testing" + mock "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "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" "github.com/grafana/grafana/pkg/setting" - mock "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" ) func TestTemplateService(t *testing.T) { diff --git a/pkg/services/ngalert/provisioning/testing.go b/pkg/services/ngalert/provisioning/testing.go index 3afb57daf87..53c34929a6a 100644 --- a/pkg/services/ngalert/provisioning/testing.go +++ b/pkg/services/ngalert/provisioning/testing.go @@ -6,8 +6,9 @@ import ( "fmt" "strings" - "github.com/grafana/grafana/pkg/services/ngalert/models" mock "github.com/stretchr/testify/mock" + + "github.com/grafana/grafana/pkg/services/ngalert/models" ) const defaultAlertmanagerConfigJSON = ` diff --git a/pkg/services/ngalert/schedule/alerts_sender_mock.go b/pkg/services/ngalert/schedule/alerts_sender_mock.go index 6f0a28eb122..cdf789a94d7 100644 --- a/pkg/services/ngalert/schedule/alerts_sender_mock.go +++ b/pkg/services/ngalert/schedule/alerts_sender_mock.go @@ -3,9 +3,9 @@ package schedule import ( - definitions "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" mock "github.com/stretchr/testify/mock" + definitions "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" models "github.com/grafana/grafana/pkg/services/ngalert/models" ) diff --git a/pkg/services/ngalert/schedule/compat.go b/pkg/services/ngalert/schedule/compat.go index 40590e8059c..282403bd4f7 100644 --- a/pkg/services/ngalert/schedule/compat.go +++ b/pkg/services/ngalert/schedule/compat.go @@ -10,11 +10,11 @@ import ( "github.com/benbjohnson/clock" "github.com/go-openapi/strfmt" + alertingModels "github.com/grafana/alerting/alerting/models" "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/prometheus/alertmanager/api/v2/models" "github.com/prometheus/common/model" - alertingModels "github.com/grafana/alerting/alerting/models" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/eval" ngModels "github.com/grafana/grafana/pkg/services/ngalert/models" @@ -152,16 +152,16 @@ func FromStateTransitionToPostableAlerts(firingStates []state.StateTransition, s return alerts } -// FromAlertsStateToStoppedAlert converts firingStates that have evaluation state either eval.Alerting or eval.NoData or eval.Error to models.PostableAlert that are accepted by notifiers. -// Returns a list of alert instances that have expiration time.Now -func FromAlertsStateToStoppedAlert(firingStates []*state.State, appURL *url.URL, clock clock.Clock) apimodels.PostableAlerts { +// FromAlertsStateToStoppedAlert selects only transitions from firing states (states eval.Alerting, eval.NoData, eval.Error) +// and converts them to models.PostableAlert with EndsAt set to time.Now +func FromAlertsStateToStoppedAlert(firingStates []state.StateTransition, appURL *url.URL, clock clock.Clock) apimodels.PostableAlerts { alerts := apimodels.PostableAlerts{PostableAlerts: make([]models.PostableAlert, 0, len(firingStates))} ts := clock.Now() - for _, alertState := range firingStates { - if alertState.State == eval.Normal || alertState.State == eval.Pending { + for _, transition := range firingStates { + if transition.PreviousState == eval.Normal || transition.PreviousState == eval.Pending { continue } - postableAlert := stateToPostableAlert(alertState, appURL) + postableAlert := stateToPostableAlert(transition.State, appURL) postableAlert.EndsAt = strfmt.DateTime(ts) alerts.PostableAlerts = append(alerts.PostableAlerts, *postableAlert) } diff --git a/pkg/services/ngalert/schedule/compat_test.go b/pkg/services/ngalert/schedule/compat_test.go index dc36ca27e1f..fa97ad58305 100644 --- a/pkg/services/ngalert/schedule/compat_test.go +++ b/pkg/services/ngalert/schedule/compat_test.go @@ -9,11 +9,11 @@ import ( "github.com/benbjohnson/clock" "github.com/go-openapi/strfmt" + alertingModels "github.com/grafana/alerting/alerting/models" "github.com/prometheus/alertmanager/api/v2/models" "github.com/prometheus/common/model" "github.com/stretchr/testify/require" - alertingModels "github.com/grafana/alerting/alerting/models" "github.com/grafana/grafana/pkg/services/ngalert/eval" ngModels "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/state" @@ -222,9 +222,14 @@ func Test_FromAlertsStateToStoppedAlert(t *testing.T) { } evalStates := [...]eval.State{eval.Normal, eval.Alerting, eval.Pending, eval.Error, eval.NoData} - states := make([]*state.State, 0, len(evalStates)) - for _, s := range evalStates { - states = append(states, randomState(s)) + states := make([]state.StateTransition, 0, len(evalStates)*len(evalStates)) + for _, to := range evalStates { + for _, from := range evalStates { + states = append(states, state.StateTransition{ + State: randomState(to), + PreviousState: from, + }) + } } clk := clock.NewMock() @@ -232,10 +237,10 @@ func Test_FromAlertsStateToStoppedAlert(t *testing.T) { expected := make([]models.PostableAlert, 0, len(states)) for _, s := range states { - if !(s.State == eval.Alerting || s.State == eval.Error || s.State == eval.NoData) { + if !(s.PreviousState == eval.Alerting || s.PreviousState == eval.Error || s.PreviousState == eval.NoData) { continue } - alert := stateToPostableAlert(s, appURL) + alert := stateToPostableAlert(s.State, appURL) alert.EndsAt = strfmt.DateTime(clk.Now()) expected = append(expected, *alert) } diff --git a/pkg/services/ngalert/schedule/schedule.go b/pkg/services/ngalert/schedule/schedule.go index f8353a23413..62a6a391842 100644 --- a/pkg/services/ngalert/schedule/schedule.go +++ b/pkg/services/ngalert/schedule/schedule.go @@ -7,11 +7,13 @@ import ( "net/url" "time" + "github.com/benbjohnson/clock" + alertingModels "github.com/grafana/alerting/alerting/models" "github.com/hashicorp/go-multierror" prometheusModel "github.com/prometheus/common/model" "go.opentelemetry.io/otel/attribute" + "golang.org/x/sync/errgroup" - alertingModels "github.com/grafana/alerting/alerting/models" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/datasources" @@ -23,9 +25,6 @@ import ( "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/util/ticker" - - "github.com/benbjohnson/clock" - "golang.org/x/sync/errgroup" ) // ScheduleService is an interface for a service that schedules the evaluation @@ -324,7 +323,7 @@ func (sch *schedule) ruleRoutine(grafanaCtx context.Context, key ngmodels.AlertR evalDuration := sch.metrics.EvalDuration.WithLabelValues(orgID) evalTotalFailures := sch.metrics.EvalFailures.WithLabelValues(orgID) - notify := func(states []*state.State) { + notify := func(states []state.StateTransition) { expiredAlerts := FromAlertsStateToStoppedAlert(states, sch.appURL, sch.clock) if len(expiredAlerts.PostableAlerts) > 0 { sch.alertsSender.Send(key, expiredAlerts) @@ -475,7 +474,11 @@ func (sch *schedule) ruleRoutine(grafanaCtx context.Context, key ngmodels.AlertR isPaused := ctx.rule.IsPaused // fetch latest alert rule version if currentRuleVersion != newVersion { - if currentRuleVersion > 0 { // do not clean up state if the eval loop has just started. + // Do not clean up state if the eval loop has just started. + // 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. + if currentRuleVersion > 0 || isPaused { logger.Debug("Got a new version of alert rule. Clear up the state and refresh extra labels", "version", currentRuleVersion, "newVersion", newVersion) resetState(grafanaCtx, isPaused) } @@ -508,7 +511,7 @@ func (sch *schedule) ruleRoutine(grafanaCtx context.Context, key ngmodels.AlertR // cases. ctx, cancelFunc := context.WithTimeout(context.Background(), time.Minute) defer cancelFunc() - states := sch.stateManager.DeleteStateByRuleUID(ngmodels.WithRuleKey(ctx, key), key) + states := sch.stateManager.DeleteStateByRuleUID(ngmodels.WithRuleKey(ctx, key), key, ngmodels.StateReasonRuleDeleted) notify(states) } logger.Debug("Stopping alert rule routine") diff --git a/pkg/services/ngalert/schedule/schedule_mock.go b/pkg/services/ngalert/schedule/schedule_mock.go index 9a950779f32..d9b1a9ac723 100644 --- a/pkg/services/ngalert/schedule/schedule_mock.go +++ b/pkg/services/ngalert/schedule/schedule_mock.go @@ -4,11 +4,11 @@ package schedule import ( context "context" + time "time" - models "github.com/grafana/grafana/pkg/services/ngalert/models" mock "github.com/stretchr/testify/mock" - time "time" + models "github.com/grafana/grafana/pkg/services/ngalert/models" ) // FakeScheduleService is an autogenerated mock type for the ScheduleService type diff --git a/pkg/services/ngalert/sender/router_test.go b/pkg/services/ngalert/sender/router_test.go index 7fd6376d8e9..daca9986768 100644 --- a/pkg/services/ngalert/sender/router_test.go +++ b/pkg/services/ngalert/sender/router_test.go @@ -10,13 +10,13 @@ import ( "github.com/benbjohnson/clock" "github.com/go-openapi/strfmt" - "github.com/grafana/grafana/pkg/infra/log/logtest" models2 "github.com/prometheus/alertmanager/api/v2/models" "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/log/logtest" "github.com/grafana/grafana/pkg/services/datasources" fake_ds "github.com/grafana/grafana/pkg/services/datasources/fakes" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" diff --git a/pkg/services/ngalert/sender/sender.go b/pkg/services/ngalert/sender/sender.go index 31ba8c2b3e2..2d3b25b5a5b 100644 --- a/pkg/services/ngalert/sender/sender.go +++ b/pkg/services/ngalert/sender/sender.go @@ -12,8 +12,6 @@ import ( "time" "unicode" - "github.com/grafana/grafana/pkg/infra/log" - apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/prometheus/alertmanager/api/v2/models" "github.com/prometheus/client_golang/prometheus" common_config "github.com/prometheus/common/config" @@ -22,6 +20,9 @@ import ( "github.com/prometheus/prometheus/discovery" "github.com/prometheus/prometheus/notifier" "github.com/prometheus/prometheus/pkg/labels" + + "github.com/grafana/grafana/pkg/infra/log" + apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" ) const ( diff --git a/pkg/services/ngalert/state/historian/annotation.go b/pkg/services/ngalert/state/historian/annotation.go index 188c9010371..bca76460e78 100644 --- a/pkg/services/ngalert/state/historian/annotation.go +++ b/pkg/services/ngalert/state/historian/annotation.go @@ -9,6 +9,7 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/annotations" diff --git a/pkg/services/ngalert/state/historian/annotation_test.go b/pkg/services/ngalert/state/historian/annotation_test.go index 25497a6d6b6..e94869c9d5a 100644 --- a/pkg/services/ngalert/state/historian/annotation_test.go +++ b/pkg/services/ngalert/state/historian/annotation_test.go @@ -5,6 +5,8 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/annotations" @@ -12,7 +14,6 @@ import ( "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/tests/fakes" - "github.com/stretchr/testify/require" ) func TestAnnotationHistorian_Integration(t *testing.T) { diff --git a/pkg/services/ngalert/state/historian/core.go b/pkg/services/ngalert/state/historian/core.go index d8d49868413..d59181cf317 100644 --- a/pkg/services/ngalert/state/historian/core.go +++ b/pkg/services/ngalert/state/historian/core.go @@ -4,6 +4,7 @@ import ( "strings" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/grafana/grafana/pkg/services/ngalert/models" @@ -12,8 +13,17 @@ import ( ) func shouldRecord(transition state.StateTransition) bool { + if !transition.Changed() { + return false + } + // Do not log not transitioned states normal states if it was marked as stale - if !transition.Changed() || transition.StateReason == models.StateReasonMissingSeries && transition.PreviousState == eval.Normal && transition.State.State == eval.Normal { + if transition.StateReason == models.StateReasonMissingSeries && transition.PreviousState == eval.Normal && transition.State.State == eval.Normal { + return false + } + // Do not log transition from Normal (Paused|Updated) to Normal + if transition.State.State == eval.Normal && transition.StateReason == "" && + transition.PreviousState == eval.Normal && (transition.PreviousStateReason == models.StateReasonPaused || transition.PreviousStateReason == models.StateReasonUpdated) { return false } return true diff --git a/pkg/services/ngalert/state/historian/core_test.go b/pkg/services/ngalert/state/historian/core_test.go index 0eae1303640..d88bb192721 100644 --- a/pkg/services/ngalert/state/historian/core_test.go +++ b/pkg/services/ngalert/state/historian/core_test.go @@ -4,9 +4,9 @@ import ( "fmt" "testing" + "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/stretchr/testify/require" - "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/state" @@ -44,10 +44,14 @@ func TestShouldRecord(t *testing.T) { knownReasons := []string{ "", models.StateReasonMissingSeries, + models.StateReasonPaused, + models.StateReasonUpdated, + models.StateReasonRuleDeleted, eval.Error.String(), eval.NoData.String(), } + // all combinations does not reflect the real transitions that could happen, which is a subset. allCombinations := make([]Transition, 0, len(allStates)*len(allStates)*len(knownReasons)*len(knownReasons)) for _, from := range allStates { for _, reasonFrom := range knownReasons { @@ -60,30 +64,23 @@ func TestShouldRecord(t *testing.T) { } negativeTransitions := map[Transition]struct{}{ - noTransition(eval.Normal, ""): {}, - noTransition(eval.Normal, eval.Error.String()): {}, - noTransition(eval.Normal, eval.NoData.String()): {}, - noTransition(eval.Normal, models.StateReasonMissingSeries): {}, - noTransition(eval.Alerting, ""): {}, - noTransition(eval.Alerting, eval.Error.String()): {}, - noTransition(eval.Alerting, eval.NoData.String()): {}, - noTransition(eval.Alerting, models.StateReasonMissingSeries): {}, - noTransition(eval.Pending, ""): {}, - noTransition(eval.Pending, eval.Error.String()): {}, - noTransition(eval.Pending, eval.NoData.String()): {}, - noTransition(eval.Pending, models.StateReasonMissingSeries): {}, - noTransition(eval.NoData, ""): {}, - noTransition(eval.NoData, eval.Error.String()): {}, - noTransition(eval.NoData, eval.NoData.String()): {}, - noTransition(eval.NoData, models.StateReasonMissingSeries): {}, - noTransition(eval.Error, ""): {}, - noTransition(eval.Error, eval.Error.String()): {}, - noTransition(eval.Error, eval.NoData.String()): {}, - noTransition(eval.Error, models.StateReasonMissingSeries): {}, - transition(eval.Normal, "", eval.Normal, models.StateReasonMissingSeries): {}, transition(eval.Normal, eval.Error.String(), eval.Normal, models.StateReasonMissingSeries): {}, transition(eval.Normal, eval.NoData.String(), eval.Normal, models.StateReasonMissingSeries): {}, + + transition(eval.Normal, models.StateReasonPaused, eval.Normal, ""): {}, + transition(eval.Normal, models.StateReasonUpdated, eval.Normal, ""): {}, + + // these transitions are actually not possible + transition(eval.Normal, models.StateReasonRuleDeleted, eval.Normal, models.StateReasonMissingSeries): {}, + transition(eval.Normal, models.StateReasonPaused, eval.Normal, models.StateReasonMissingSeries): {}, + transition(eval.Normal, models.StateReasonUpdated, eval.Normal, models.StateReasonMissingSeries): {}, + } + // add all transitions from reason X(Y) to X(Y) as negative. + for _, s := range allStates { + for _, reason := range knownReasons { + negativeTransitions[noTransition(s, reason)] = struct{}{} + } } for _, tc := range allCombinations { diff --git a/pkg/services/ngalert/state/historian/dashboard.go b/pkg/services/ngalert/state/historian/dashboard.go index 74b097298e2..e5e4b9f2d34 100644 --- a/pkg/services/ngalert/state/historian/dashboard.go +++ b/pkg/services/ngalert/state/historian/dashboard.go @@ -7,10 +7,11 @@ import ( "strconv" "time" - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/services/dashboards" "github.com/patrickmn/go-cache" "golang.org/x/sync/singleflight" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/dashboards" ) const ( diff --git a/pkg/services/ngalert/state/historian/dashboard_test.go b/pkg/services/ngalert/state/historian/dashboard_test.go index f455ef0f70d..34410851043 100644 --- a/pkg/services/ngalert/state/historian/dashboard_test.go +++ b/pkg/services/ngalert/state/historian/dashboard_test.go @@ -5,9 +5,10 @@ import ( "testing" "time" - "github.com/grafana/grafana/pkg/services/dashboards" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/services/dashboards" ) func TestDashboardResolver(t *testing.T) { diff --git a/pkg/services/ngalert/state/historian/loki.go b/pkg/services/ngalert/state/historian/loki.go index 466fc6137c4..f3bd9f5661d 100644 --- a/pkg/services/ngalert/state/historian/loki.go +++ b/pkg/services/ngalert/state/historian/loki.go @@ -7,6 +7,7 @@ import ( "sort" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/ngalert/eval" diff --git a/pkg/services/ngalert/state/historian/loki_http_test.go b/pkg/services/ngalert/state/historian/loki_http_test.go index f9ec280e4f1..e5834dbc31a 100644 --- a/pkg/services/ngalert/state/historian/loki_http_test.go +++ b/pkg/services/ngalert/state/historian/loki_http_test.go @@ -5,8 +5,9 @@ import ( "net/url" "testing" - "github.com/grafana/grafana/pkg/infra/log" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/infra/log" ) // This function can be used for local testing, just remove the skip call. diff --git a/pkg/services/ngalert/state/historian/model/rule_test.go b/pkg/services/ngalert/state/historian/model/rule_test.go index 228e9537307..df86f032389 100644 --- a/pkg/services/ngalert/state/historian/model/rule_test.go +++ b/pkg/services/ngalert/state/historian/model/rule_test.go @@ -3,9 +3,10 @@ package model import ( "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/ngalert/models" - "github.com/stretchr/testify/require" ) func TestNewRuleMeta(t *testing.T) { diff --git a/pkg/services/ngalert/state/historian/query.go b/pkg/services/ngalert/state/historian/query.go index 26849c88a5f..2648851d9fe 100644 --- a/pkg/services/ngalert/state/historian/query.go +++ b/pkg/services/ngalert/state/historian/query.go @@ -4,6 +4,7 @@ import ( "context" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/services/ngalert/models" ) diff --git a/pkg/services/ngalert/state/historian/sql.go b/pkg/services/ngalert/state/historian/sql.go index 64e716204b8..8dc1b435e9a 100644 --- a/pkg/services/ngalert/state/historian/sql.go +++ b/pkg/services/ngalert/state/historian/sql.go @@ -4,6 +4,7 @@ import ( "context" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/state" diff --git a/pkg/services/ngalert/state/image_mock.go b/pkg/services/ngalert/state/image_mock.go index 62913c5c05c..f2cc71e5204 100644 --- a/pkg/services/ngalert/state/image_mock.go +++ b/pkg/services/ngalert/state/image_mock.go @@ -9,6 +9,7 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" + models "github.com/grafana/grafana/pkg/services/ngalert/models" ) diff --git a/pkg/services/ngalert/state/manager.go b/pkg/services/ngalert/state/manager.go index 9d222fda1f2..76e65c06434 100644 --- a/pkg/services/ngalert/state/manager.go +++ b/pkg/services/ngalert/state/manager.go @@ -7,6 +7,7 @@ import ( "github.com/benbjohnson/clock" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/grafana/grafana/pkg/services/ngalert/metrics" @@ -167,14 +168,38 @@ func (st *Manager) Get(orgID int64, alertRuleUID, stateId string) *State { // 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. -func (st *Manager) DeleteStateByRuleUID(ctx context.Context, ruleKey ngModels.AlertRuleKey) []*State { - logger := st.log.New(ruleKey.LogContext()...) +func (st *Manager) DeleteStateByRuleUID(ctx context.Context, ruleKey ngModels.AlertRuleKey, reason string) []StateTransition { + logger := st.log.FromContext(ctx) logger.Debug("Resetting state of the rule") states := st.cache.removeByRuleUID(ruleKey.OrgID, ruleKey.UID) + if len(states) == 0 { - return states + return nil } + + now := st.clock.Now() + transitions := make([]StateTransition, 0, len(states)) + for _, s := range states { + oldState := s.State + oldReason := s.StateReason + startsAt := s.StartsAt + if s.State != eval.Normal { + startsAt = now + } + s.SetNormal(reason, startsAt, now) + // Set Resolved property so the scheduler knows to send a postable alert + // to Alertmanager. + s.Resolved = oldState == eval.Alerting + s.LastEvaluationTime = now + s.Values = map[string]float64{} + transitions = append(transitions, StateTransition{ + State: s, + PreviousState: oldState, + PreviousStateReason: oldReason, + }) + } + if st.instanceStore != nil { err := st.instanceStore.DeleteAlertInstancesByRule(ctx, ruleKey) if err != nil { @@ -183,32 +208,17 @@ func (st *Manager) DeleteStateByRuleUID(ctx context.Context, ruleKey ngModels.Al } logger.Info("Rules state was reset", "states", len(states)) - return states + return transitions } // 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) []*State { +func (st *Manager) ResetStateByRuleUID(ctx context.Context, rule *ngModels.AlertRule, reason string) []StateTransition { ruleKey := rule.GetKey() - states := st.DeleteStateByRuleUID(ctx, ruleKey) + transitions := st.DeleteStateByRuleUID(ctx, ruleKey, reason) - if rule == nil || st.historian == nil { - return states - } - transitions := make([]StateTransition, 0, len(states)) - for _, s := range states { - oldState := s.State - oldReason := s.StateReason - state := *s - now := time.Now() - state.SetNormal(reason, s.StartsAt, now) - state.LastEvaluationTime = now - state.Values = map[string]float64{} - transitions = append(transitions, StateTransition{ - State: &state, - PreviousState: oldState, - PreviousStateReason: oldReason, - }) + if rule == nil || st.historian == nil || len(transitions) == 0 { + return transitions } ruleMeta := history_model.NewRuleMeta(rule, st.log) @@ -219,7 +229,7 @@ func (st *Manager) ResetStateByRuleUID(ctx context.Context, rule *ngModels.Alert st.log.FromContext(ctx).Error("Error updating historian state reset transitions", append(ruleKey.LogContext(), "reason", reason, "error", err)...) } }() - return states + return transitions } // ProcessEvalResults updates the current states that belong to a rule with the evaluation results. diff --git a/pkg/services/ngalert/state/manager_test.go b/pkg/services/ngalert/state/manager_test.go index 047c861ca80..48fc6998f23 100644 --- a/pkg/services/ngalert/state/manager_test.go +++ b/pkg/services/ngalert/state/manager_test.go @@ -27,6 +27,7 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/state" "github.com/grafana/grafana/pkg/services/ngalert/state/historian" "github.com/grafana/grafana/pkg/services/ngalert/tests" + "github.com/grafana/grafana/pkg/util" ) var testMetrics = metrics.NewNGAlert(prometheus.NewPedanticRegistry()) @@ -2623,12 +2624,14 @@ func TestDeleteStateByRuleUID(t *testing.T) { for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { ctx := context.Background() + clk := clock.NewMock() + clk.Set(time.Now()) cfg := state.ManagerCfg{ Metrics: testMetrics.GetStateMetrics(), ExternalURL: nil, InstanceStore: dbstore, Images: &state.NoopImageService{}, - Clock: clock.New(), + Clock: clk, Historian: &state.FakeHistorian{}, } st := state.NewManager(cfg) @@ -2641,12 +2644,28 @@ func TestDeleteStateByRuleUID(t *testing.T) { assert.Equal(t, tc.startingStateCacheCount, len(existingStatesForRule)) assert.Equal(t, tc.startingInstanceDBCount, len(q.Result)) - states := st.DeleteStateByRuleUID(ctx, rule.GetKey()) + expectedReason := util.GenerateShortUID() + transitions := st.DeleteStateByRuleUID(ctx, rule.GetKey(), expectedReason) // Check that the deleted states are the same as the ones that were in cache - assert.Equal(t, tc.startingStateCacheCount, len(states)) - for _, s := range states { - assert.Equal(t, tc.expectedStates[s.CacheID], s) + assert.Equal(t, tc.startingStateCacheCount, len(transitions)) + for _, s := range transitions { + assert.Contains(t, tc.expectedStates, s.CacheID) + oldState := tc.expectedStates[s.CacheID] + assert.Equal(t, oldState.State, s.PreviousState) + assert.Equal(t, oldState.StateReason, s.PreviousStateReason) + assert.Equal(t, eval.Normal, s.State.State) + assert.Equal(t, expectedReason, s.StateReason) + if oldState.State == eval.Normal { + assert.Equal(t, oldState.StartsAt, s.StartsAt) + assert.False(t, s.Resolved) + } else { + assert.Equal(t, clk.Now(), s.StartsAt) + if oldState.State == eval.Alerting { + assert.True(t, s.Resolved) + } + } + assert.Equal(t, clk.Now(), s.EndsAt) } q = &models.ListAlertInstancesQuery{RuleOrgID: rule.OrgID, RuleUID: rule.UID} @@ -2742,12 +2761,14 @@ func TestResetStateByRuleUID(t *testing.T) { t.Run(tc.desc, func(t *testing.T) { ctx := context.Background() fakeHistorian := &state.FakeHistorian{StateTransitions: make([]state.StateTransition, 0)} + clk := clock.NewMock() + clk.Set(time.Now()) cfg := state.ManagerCfg{ Metrics: testMetrics.GetStateMetrics(), ExternalURL: nil, InstanceStore: dbstore, Images: &state.NoopImageService{}, - Clock: clock.New(), + Clock: clk, Historian: fakeHistorian, } st := state.NewManager(cfg) @@ -2760,20 +2781,32 @@ func TestResetStateByRuleUID(t *testing.T) { assert.Equal(t, tc.startingStateCacheCount, len(existingStatesForRule)) assert.Equal(t, tc.startingInstanceDBCount, len(q.Result)) - states := st.ResetStateByRuleUID(ctx, rule, models.StateReasonPaused) + transitions := st.ResetStateByRuleUID(ctx, rule, models.StateReasonPaused) // Check that the deleted states are the same as the ones that were in cache - assert.Equal(t, tc.startingStateCacheCount, len(states)) - for _, s := range states { - assert.Equal(t, tc.expectedStates[s.CacheID], s) + assert.Equal(t, tc.startingStateCacheCount, len(transitions)) + for _, s := range transitions { + assert.Contains(t, tc.expectedStates, s.CacheID) + oldState := tc.expectedStates[s.CacheID] + assert.Equal(t, oldState.State, s.PreviousState) + assert.Equal(t, oldState.StateReason, s.PreviousStateReason) + assert.Equal(t, eval.Normal, s.State.State) + assert.Equal(t, models.StateReasonPaused, s.StateReason) + if oldState.State == eval.Normal { + assert.Equal(t, oldState.StartsAt, s.StartsAt) + assert.False(t, s.Resolved) + } else { + assert.Equal(t, clk.Now(), s.StartsAt) + if oldState.State == eval.Alerting { + assert.True(t, s.Resolved) + } + } + assert.Equal(t, clk.Now(), s.EndsAt) } // Check if both entries have been added to the historian assert.Equal(t, tc.newHistorianEntriesCount, len(fakeHistorian.StateTransitions)) - for _, str := range fakeHistorian.StateTransitions { - assert.Equal(t, tc.expectedStates[str.State.CacheID].State, str.PreviousState) - assert.Equal(t, tc.expectedStates[str.State.CacheID].StateReason, str.PreviousStateReason) - } + assert.Equal(t, transitions, fakeHistorian.StateTransitions) q = &models.ListAlertInstancesQuery{RuleOrgID: rule.OrgID, RuleUID: rule.UID} _ = dbstore.ListAlertInstances(ctx, q) diff --git a/pkg/services/ngalert/store/admin_configuration_store_mock.go b/pkg/services/ngalert/store/admin_configuration_store_mock.go index 755f16a08af..f57d00592d8 100644 --- a/pkg/services/ngalert/store/admin_configuration_store_mock.go +++ b/pkg/services/ngalert/store/admin_configuration_store_mock.go @@ -3,8 +3,9 @@ package store import ( - models "github.com/grafana/grafana/pkg/services/ngalert/models" mock "github.com/stretchr/testify/mock" + + models "github.com/grafana/grafana/pkg/services/ngalert/models" ) // AdminConfigurationStoreMock is an autogenerated mock type for the AdminConfigurationStore type diff --git a/pkg/services/ngalert/store/alert_rule.go b/pkg/services/ngalert/store/alert_rule.go index 74e7b2f8387..adfdf0337e3 100644 --- a/pkg/services/ngalert/store/alert_rule.go +++ b/pkg/services/ngalert/store/alert_rule.go @@ -7,11 +7,11 @@ import ( "strings" "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/guardian" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/services/search/model" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore/searchstore" "github.com/grafana/grafana/pkg/services/user" @@ -333,7 +333,7 @@ func (st DBstore) GetUserVisibleNamespaces(ctx context.Context, orgID int64, use Type: searchstore.TypeAlertFolder, Limit: -1, Permission: dashboards.PERMISSION_VIEW, - Sort: models.SortOption{}, + Sort: model.SortOption{}, Filters: []interface{}{ searchstore.FolderWithAlertsFilter{}, }, diff --git a/pkg/services/ngalert/store/deltas_test.go b/pkg/services/ngalert/store/deltas_test.go index e9bc7b947ba..f802e3509e6 100644 --- a/pkg/services/ngalert/store/deltas_test.go +++ b/pkg/services/ngalert/store/deltas_test.go @@ -7,13 +7,14 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/exp/rand" + "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/tests/fakes" "github.com/grafana/grafana/pkg/util" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "golang.org/x/exp/rand" ) func TestCalculateChanges(t *testing.T) { diff --git a/pkg/services/ngalert/store/provisioning_store_test.go b/pkg/services/ngalert/store/provisioning_store_test.go index e09a9bde51a..568f870027d 100644 --- a/pkg/services/ngalert/store/provisioning_store_test.go +++ b/pkg/services/ngalert/store/provisioning_store_test.go @@ -4,12 +4,13 @@ import ( "context" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/services/ngalert" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/provisioning" "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/services/ngalert/tests" - "github.com/stretchr/testify/require" ) const testAlertingIntervalSeconds = 10 diff --git a/pkg/services/ngalert/tests/util.go b/pkg/services/ngalert/tests/util.go index 2d07d32d163..18313f8d3d0 100644 --- a/pkg/services/ngalert/tests/util.go +++ b/pkg/services/ngalert/tests/util.go @@ -87,7 +87,7 @@ func SetupTestEnv(tb testing.TB, baseInterval time.Duration) (*ngalert.AlertNG, tracer := tracing.InitializeTracerForTest() bus := bus.ProvideBus(tracer) - folderService := folderimpl.ProvideService(ac, bus, cfg, dashboardStore, dashboardStore, nil, features, folderPermissions) + folderService := folderimpl.ProvideService(ac, bus, cfg, dashboardStore, dashboardStore, nil, features) ng, err := ngalert.ProvideService( cfg, featuremgmt.WithFeatures(), nil, nil, routing.NewRouteRegister(), sqlStore, nil, nil, nil, quotatest.New(false, nil), diff --git a/pkg/services/notifications/codes_test.go b/pkg/services/notifications/codes_test.go index 515a31b734d..fbcfce51e19 100644 --- a/pkg/services/notifications/codes_test.go +++ b/pkg/services/notifications/codes_test.go @@ -6,10 +6,10 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" - - "github.com/stretchr/testify/require" ) func TestTimeLimitCodes(t *testing.T) { diff --git a/pkg/services/notifications/send_email_integration_test.go b/pkg/services/notifications/send_email_integration_test.go index 99f0cee48ab..482848f03b5 100644 --- a/pkg/services/notifications/send_email_integration_test.go +++ b/pkg/services/notifications/send_email_integration_test.go @@ -5,9 +5,9 @@ import ( "os" "testing" - "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/setting" ) func TestEmailIntegrationTest(t *testing.T) { diff --git a/pkg/services/notifications/smtp.go b/pkg/services/notifications/smtp.go index 175f4e8dc13..c012a11d5c0 100644 --- a/pkg/services/notifications/smtp.go +++ b/pkg/services/notifications/smtp.go @@ -8,8 +8,9 @@ import ( "strconv" "strings" - "github.com/grafana/grafana/pkg/setting" gomail "gopkg.in/mail.v2" + + "github.com/grafana/grafana/pkg/setting" ) type SmtpClient struct { diff --git a/pkg/services/notifications/smtp_test.go b/pkg/services/notifications/smtp_test.go index 8ca5a11ed5d..fe2edd6cc6b 100644 --- a/pkg/services/notifications/smtp_test.go +++ b/pkg/services/notifications/smtp_test.go @@ -5,9 +5,10 @@ import ( "strings" "testing" - "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/setting" ) func TestBuildMail(t *testing.T) { diff --git a/pkg/services/oauthtoken/oauth_token.go b/pkg/services/oauthtoken/oauth_token.go index f2fc1118ac5..c773abdab2c 100644 --- a/pkg/services/oauthtoken/oauth_token.go +++ b/pkg/services/oauthtoken/oauth_token.go @@ -12,7 +12,6 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/login/social" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/user" @@ -38,9 +37,9 @@ type Service struct { type OAuthTokenService interface { GetCurrentOAuthToken(context.Context, *user.SignedInUser) *oauth2.Token IsOAuthPassThruEnabled(*datasources.DataSource) bool - HasOAuthEntry(context.Context, *user.SignedInUser) (*models.UserAuth, bool, error) - TryTokenRefresh(context.Context, *models.UserAuth) error - InvalidateOAuthTokens(context.Context, *models.UserAuth) error + HasOAuthEntry(context.Context, *user.SignedInUser) (*login.UserAuth, bool, error) + TryTokenRefresh(context.Context, *login.UserAuth) error + InvalidateOAuthTokens(context.Context, *login.UserAuth) error } func ProvideService(socialService social.Service, authInfoService login.AuthInfoService, cfg *setting.Cfg) *Service { @@ -59,7 +58,7 @@ func (o *Service) GetCurrentOAuthToken(ctx context.Context, usr *user.SignedInUs return nil } - authInfoQuery := &models.GetAuthInfoQuery{UserId: usr.UserID} + authInfoQuery := &login.GetAuthInfoQuery{UserId: usr.UserID} if err := o.AuthInfoService.GetAuthInfo(ctx, authInfoQuery); err != nil { if errors.Is(err, user.ErrUserNotFound) { // Not necessarily an error. User may be logged in another way. @@ -88,13 +87,13 @@ func (o *Service) IsOAuthPassThruEnabled(ds *datasources.DataSource) bool { } // HasOAuthEntry returns true and the UserAuth object when OAuth info exists for the specified User -func (o *Service) HasOAuthEntry(ctx context.Context, usr *user.SignedInUser) (*models.UserAuth, bool, error) { +func (o *Service) HasOAuthEntry(ctx context.Context, usr *user.SignedInUser) (*login.UserAuth, bool, error) { if usr == nil { // No user, therefore no token return nil, false, nil } - authInfoQuery := &models.GetAuthInfoQuery{UserId: usr.UserID} + authInfoQuery := &login.GetAuthInfoQuery{UserId: usr.UserID} err := o.AuthInfoService.GetAuthInfo(ctx, authInfoQuery) if err != nil { if errors.Is(err, user.ErrUserNotFound) { @@ -112,7 +111,7 @@ func (o *Service) HasOAuthEntry(ctx context.Context, usr *user.SignedInUser) (*m // TryTokenRefresh returns an error in case the OAuth token refresh was unsuccessful // It uses a singleflight.Group to prevent getting the Refresh Token multiple times for a given User -func (o *Service) TryTokenRefresh(ctx context.Context, usr *models.UserAuth) error { +func (o *Service) TryTokenRefresh(ctx context.Context, usr *login.UserAuth) error { lockKey := fmt.Sprintf("oauth-refresh-token-%d", usr.UserId) _, err, _ := o.singleFlightGroup.Do(lockKey, func() (interface{}, error) { logger.Debug("singleflight request for getting a new access token", "key", lockKey) @@ -122,7 +121,7 @@ func (o *Service) TryTokenRefresh(ctx context.Context, usr *models.UserAuth) err return err } -func buildOAuthTokenFromAuthInfo(authInfo *models.UserAuth) *oauth2.Token { +func buildOAuthTokenFromAuthInfo(authInfo *login.UserAuth) *oauth2.Token { token := &oauth2.Token{ AccessToken: authInfo.OAuthAccessToken, Expiry: authInfo.OAuthExpiry, @@ -137,7 +136,7 @@ func buildOAuthTokenFromAuthInfo(authInfo *models.UserAuth) *oauth2.Token { return token } -func checkOAuthRefreshToken(authInfo *models.UserAuth) error { +func checkOAuthRefreshToken(authInfo *login.UserAuth) error { if !strings.Contains(authInfo.AuthModule, "oauth") { logger.Warn("the specified user's auth provider is not oauth", "authmodule", authInfo.AuthModule, "userid", authInfo.UserId) @@ -154,8 +153,8 @@ func checkOAuthRefreshToken(authInfo *models.UserAuth) error { } // InvalidateOAuthTokens invalidates the OAuth tokens (access_token, refresh_token) and sets the Expiry to default/zero -func (o *Service) InvalidateOAuthTokens(ctx context.Context, usr *models.UserAuth) error { - return o.AuthInfoService.UpdateAuthInfo(ctx, &models.UpdateAuthInfoCommand{ +func (o *Service) InvalidateOAuthTokens(ctx context.Context, usr *login.UserAuth) error { + return o.AuthInfoService.UpdateAuthInfo(ctx, &login.UpdateAuthInfoCommand{ UserId: usr.UserId, AuthModule: usr.AuthModule, AuthId: usr.AuthId, @@ -167,7 +166,7 @@ func (o *Service) InvalidateOAuthTokens(ctx context.Context, usr *models.UserAut }) } -func (o *Service) tryGetOrRefreshAccessToken(ctx context.Context, usr *models.UserAuth) (*oauth2.Token, error) { +func (o *Service) tryGetOrRefreshAccessToken(ctx context.Context, usr *login.UserAuth) (*oauth2.Token, error) { if err := checkOAuthRefreshToken(usr); err != nil { return nil, err } @@ -198,7 +197,7 @@ func (o *Service) tryGetOrRefreshAccessToken(ctx context.Context, usr *models.Us // If the tokens are not the same, update the entry in the DB if !tokensEq(persistedToken, token) { - updateAuthCommand := &models.UpdateAuthInfoCommand{ + updateAuthCommand := &login.UpdateAuthInfoCommand{ UserId: usr.UserId, AuthModule: usr.AuthModule, AuthId: usr.AuthId, diff --git a/pkg/services/oauthtoken/oauth_token_test.go b/pkg/services/oauthtoken/oauth_token_test.go index 063de3ce3f2..1392d1e229e 100644 --- a/pkg/services/oauthtoken/oauth_token_test.go +++ b/pkg/services/oauthtoken/oauth_token_test.go @@ -8,29 +8,29 @@ import ( "testing" "time" - "github.com/grafana/grafana/pkg/infra/usagestats" - "github.com/grafana/grafana/pkg/login/social" - "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/login" - "github.com/grafana/grafana/pkg/services/login/authinfoservice" - "github.com/grafana/grafana/pkg/services/user" - "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "golang.org/x/oauth2" "golang.org/x/sync/singleflight" + + "github.com/grafana/grafana/pkg/infra/usagestats" + "github.com/grafana/grafana/pkg/login/social" + "github.com/grafana/grafana/pkg/services/login" + "github.com/grafana/grafana/pkg/services/login/authinfoservice" + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/setting" ) func TestService_HasOAuthEntry(t *testing.T) { testCases := []struct { name string user *user.SignedInUser - want *models.UserAuth + want *login.UserAuth wantExist bool wantErr bool err error getAuthInfoErr error - getAuthInfoUser models.UserAuth + getAuthInfoUser login.UserAuth }{ { name: "returns false without an error in case user is nil", @@ -61,15 +61,15 @@ func TestService_HasOAuthEntry(t *testing.T) { want: nil, wantExist: false, wantErr: false, - getAuthInfoUser: models.UserAuth{AuthModule: "auth_saml"}, + getAuthInfoUser: login.UserAuth{AuthModule: "auth_saml"}, }, { name: "returns true when the auth entry is found", user: &user.SignedInUser{}, - want: &models.UserAuth{AuthModule: "oauth_generic_oauth"}, + want: &login.UserAuth{AuthModule: "oauth_generic_oauth"}, wantExist: true, wantErr: false, - getAuthInfoUser: models.UserAuth{AuthModule: "oauth_generic_oauth"}, + getAuthInfoUser: login.UserAuth{AuthModule: "oauth_generic_oauth"}, }, } for _, tc := range testCases { @@ -101,7 +101,7 @@ func TestService_TryTokenRefresh_ValidToken(t *testing.T) { Expiry: time.Now(), TokenType: "Bearer", } - usr := &models.UserAuth{ + usr := &login.UserAuth{ AuthModule: "oauth_generic_oauth", OAuthAccessToken: token.AccessToken, OAuthRefreshToken: token.RefreshToken, @@ -117,7 +117,7 @@ func TestService_TryTokenRefresh_ValidToken(t *testing.T) { assert.Nil(t, err) socialConnector.AssertNumberOfCalls(t, "TokenSource", 1) - authInfoQuery := &models.GetAuthInfoQuery{} + authInfoQuery := &login.GetAuthInfoQuery{} err = srv.AuthInfoService.GetAuthInfo(ctx, authInfoQuery) assert.Nil(t, err) @@ -139,7 +139,7 @@ func TestService_TryTokenRefresh_NoRefreshToken(t *testing.T) { Expiry: time.Now().Add(-time.Hour), TokenType: "Bearer", } - usr := &models.UserAuth{ + usr := &login.UserAuth{ AuthModule: "oauth_generic_oauth", OAuthAccessToken: token.AccessToken, OAuthRefreshToken: token.RefreshToken, @@ -174,7 +174,7 @@ func TestService_TryTokenRefresh_ExpiredToken(t *testing.T) { TokenType: "Bearer", } - usr := &models.UserAuth{ + usr := &login.UserAuth{ AuthModule: "oauth_generic_oauth", OAuthAccessToken: token.AccessToken, OAuthRefreshToken: token.RefreshToken, @@ -191,7 +191,7 @@ func TestService_TryTokenRefresh_ExpiredToken(t *testing.T) { assert.Nil(t, err) socialConnector.AssertNumberOfCalls(t, "TokenSource", 1) - authInfoQuery := &models.GetAuthInfoQuery{} + authInfoQuery := &login.GetAuthInfoQuery{} err = srv.AuthInfoService.GetAuthInfo(ctx, authInfoQuery) assert.Nil(t, err) @@ -207,7 +207,7 @@ func TestService_TryTokenRefresh_DifferentAuthModuleForUser(t *testing.T) { srv, _, socialConnector := setupOAuthTokenService(t) ctx := context.Background() token := &oauth2.Token{} - usr := &models.UserAuth{ + usr := &login.UserAuth{ AuthModule: "auth.saml", } @@ -307,30 +307,30 @@ type FakeAuthInfoStore struct { login.Store ExpectedError error ExpectedUser *user.User - ExpectedOAuth *models.UserAuth + ExpectedOAuth *login.UserAuth ExpectedDuplicateUserEntries int ExpectedHasDuplicateUserEntries int ExpectedLoginStats login.LoginStats } -func (f *FakeAuthInfoStore) GetExternalUserInfoByLogin(ctx context.Context, query *models.GetExternalUserInfoByLoginQuery) error { +func (f *FakeAuthInfoStore) GetExternalUserInfoByLogin(ctx context.Context, query *login.GetExternalUserInfoByLoginQuery) error { return f.ExpectedError } -func (f *FakeAuthInfoStore) GetAuthInfo(ctx context.Context, query *models.GetAuthInfoQuery) error { +func (f *FakeAuthInfoStore) GetAuthInfo(ctx context.Context, query *login.GetAuthInfoQuery) error { query.Result = f.ExpectedOAuth return f.ExpectedError } -func (f *FakeAuthInfoStore) SetAuthInfo(ctx context.Context, cmd *models.SetAuthInfoCommand) error { +func (f *FakeAuthInfoStore) SetAuthInfo(ctx context.Context, cmd *login.SetAuthInfoCommand) error { return f.ExpectedError } -func (f *FakeAuthInfoStore) UpdateAuthInfoDate(ctx context.Context, authInfo *models.UserAuth) error { +func (f *FakeAuthInfoStore) UpdateAuthInfoDate(ctx context.Context, authInfo *login.UserAuth) error { return f.ExpectedError } -func (f *FakeAuthInfoStore) UpdateAuthInfo(ctx context.Context, cmd *models.UpdateAuthInfoCommand) error { +func (f *FakeAuthInfoStore) UpdateAuthInfo(ctx context.Context, cmd *login.UpdateAuthInfoCommand) error { f.ExpectedOAuth.OAuthAccessToken = cmd.OAuthToken.AccessToken f.ExpectedOAuth.OAuthExpiry = cmd.OAuthToken.Expiry f.ExpectedOAuth.OAuthTokenType = cmd.OAuthToken.TokenType @@ -338,7 +338,7 @@ func (f *FakeAuthInfoStore) UpdateAuthInfo(ctx context.Context, cmd *models.Upda return f.ExpectedError } -func (f *FakeAuthInfoStore) DeleteAuthInfo(ctx context.Context, cmd *models.DeleteAuthInfoCommand) error { +func (f *FakeAuthInfoStore) DeleteAuthInfo(ctx context.Context, cmd *login.DeleteAuthInfoCommand) error { return f.ExpectedError } diff --git a/pkg/services/oauthtoken/oauthtokentest/mock.go b/pkg/services/oauthtoken/oauthtokentest/mock.go index 95bc1ccd204..e1af309faa3 100644 --- a/pkg/services/oauthtoken/oauthtokentest/mock.go +++ b/pkg/services/oauthtoken/oauthtokentest/mock.go @@ -3,18 +3,19 @@ package oauthtokentest import ( "context" - "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/datasources" - "github.com/grafana/grafana/pkg/services/user" "golang.org/x/oauth2" + + "github.com/grafana/grafana/pkg/services/datasources" + "github.com/grafana/grafana/pkg/services/login" + "github.com/grafana/grafana/pkg/services/user" ) type MockOauthTokenService struct { GetCurrentOauthTokenFunc func(ctx context.Context, usr *user.SignedInUser) *oauth2.Token IsOAuthPassThruEnabledFunc func(ds *datasources.DataSource) bool - HasOAuthEntryFunc func(ctx context.Context, usr *user.SignedInUser) (*models.UserAuth, bool, error) - InvalidateOAuthTokensFunc func(ctx context.Context, usr *models.UserAuth) error - TryTokenRefreshFunc func(ctx context.Context, usr *models.UserAuth) error + HasOAuthEntryFunc func(ctx context.Context, usr *user.SignedInUser) (*login.UserAuth, bool, error) + InvalidateOAuthTokensFunc func(ctx context.Context, usr *login.UserAuth) error + TryTokenRefreshFunc func(ctx context.Context, usr *login.UserAuth) error } func (m *MockOauthTokenService) GetCurrentOAuthToken(ctx context.Context, usr *user.SignedInUser) *oauth2.Token { @@ -31,21 +32,21 @@ func (m *MockOauthTokenService) IsOAuthPassThruEnabled(ds *datasources.DataSourc return false } -func (m *MockOauthTokenService) HasOAuthEntry(ctx context.Context, usr *user.SignedInUser) (*models.UserAuth, bool, error) { +func (m *MockOauthTokenService) HasOAuthEntry(ctx context.Context, usr *user.SignedInUser) (*login.UserAuth, bool, error) { if m.HasOAuthEntryFunc != nil { return m.HasOAuthEntryFunc(ctx, usr) } return nil, false, nil } -func (m *MockOauthTokenService) InvalidateOAuthTokens(ctx context.Context, usr *models.UserAuth) error { +func (m *MockOauthTokenService) InvalidateOAuthTokens(ctx context.Context, usr *login.UserAuth) error { if m.InvalidateOAuthTokensFunc != nil { return m.InvalidateOAuthTokensFunc(ctx, usr) } return nil } -func (m *MockOauthTokenService) TryTokenRefresh(ctx context.Context, usr *models.UserAuth) error { +func (m *MockOauthTokenService) TryTokenRefresh(ctx context.Context, usr *login.UserAuth) error { if m.TryTokenRefreshFunc != nil { return m.TryTokenRefreshFunc(ctx, usr) } diff --git a/pkg/services/oauthtoken/oauthtokentest/oauthtokentest.go b/pkg/services/oauthtoken/oauthtokentest/oauthtokentest.go index 230b2d7e290..2bb990c8446 100644 --- a/pkg/services/oauthtoken/oauthtokentest/oauthtokentest.go +++ b/pkg/services/oauthtoken/oauthtokentest/oauthtokentest.go @@ -3,11 +3,12 @@ package oauthtokentest import ( "context" - "github.com/grafana/grafana/pkg/models" + "golang.org/x/oauth2" + "github.com/grafana/grafana/pkg/services/datasources" + "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/oauthtoken" "github.com/grafana/grafana/pkg/services/user" - "golang.org/x/oauth2" ) // Service an OAuth token service suitable for tests. @@ -28,14 +29,14 @@ func (s *Service) IsOAuthPassThruEnabled(ds *datasources.DataSource) bool { return oauthtoken.IsOAuthPassThruEnabled(ds) } -func (s *Service) HasOAuthEntry(context.Context, *user.SignedInUser) (*models.UserAuth, bool, error) { +func (s *Service) HasOAuthEntry(context.Context, *user.SignedInUser) (*login.UserAuth, bool, error) { return nil, false, nil } -func (s *Service) TryTokenRefresh(context.Context, *models.UserAuth) error { +func (s *Service) TryTokenRefresh(context.Context, *login.UserAuth) error { return nil } -func (s *Service) InvalidateOAuthTokens(context.Context, *models.UserAuth) error { +func (s *Service) InvalidateOAuthTokens(context.Context, *login.UserAuth) error { return nil } diff --git a/pkg/services/org/orgimpl/org_test.go b/pkg/services/org/orgimpl/org_test.go index 206e6d2c016..952a96fab88 100644 --- a/pkg/services/org/orgimpl/org_test.go +++ b/pkg/services/org/orgimpl/org_test.go @@ -4,10 +4,11 @@ import ( "context" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/require" ) func TestOrgService(t *testing.T) { diff --git a/pkg/services/plugindashboards/service/service_test.go b/pkg/services/plugindashboards/service/service_test.go index 2349602ebdc..6cfed39d71f 100644 --- a/pkg/services/plugindashboards/service/service_test.go +++ b/pkg/services/plugindashboards/service/service_test.go @@ -8,12 +8,13 @@ import ( "sort" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/manager/dashboards" dashmodels "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/plugindashboards" - "github.com/stretchr/testify/require" ) func TestGetPluginDashboards(t *testing.T) { diff --git a/pkg/services/pluginsintegration/pluginsintegration.go b/pkg/services/pluginsintegration/pluginsintegration.go index f6433487d20..ba9937438ab 100644 --- a/pkg/services/pluginsintegration/pluginsintegration.go +++ b/pkg/services/pluginsintegration/pluginsintegration.go @@ -10,11 +10,13 @@ import ( "github.com/grafana/grafana/pkg/plugins/manager" "github.com/grafana/grafana/pkg/plugins/manager/client" "github.com/grafana/grafana/pkg/plugins/manager/loader" + "github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" "github.com/grafana/grafana/pkg/plugins/manager/process" "github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/manager/signature" "github.com/grafana/grafana/pkg/plugins/manager/store" "github.com/grafana/grafana/pkg/plugins/plugincontext" + "github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/plugins/repo" "github.com/grafana/grafana/pkg/services/oauthtoken" "github.com/grafana/grafana/pkg/services/pluginsintegration/clientmiddleware" @@ -34,6 +36,8 @@ var WireSet = wire.NewSet( process.ProvideService, wire.Bind(new(process.Service), new(*process.Manager)), coreplugin.ProvideCoreRegistry, + pluginscdn.ProvideService, + assetpath.ProvideService, loader.ProvideService, wire.Bind(new(loader.Service), new(*loader.Loader)), wire.Bind(new(plugins.ErrorResolver), new(*loader.Loader)), diff --git a/pkg/services/provisioning/alerting/config_reader.go b/pkg/services/provisioning/alerting/config_reader.go index 489e5b5776b..b6ca9d2c3dd 100644 --- a/pkg/services/provisioning/alerting/config_reader.go +++ b/pkg/services/provisioning/alerting/config_reader.go @@ -8,8 +8,9 @@ import ( "path/filepath" "strings" - "github.com/grafana/grafana/pkg/infra/log" "gopkg.in/yaml.v3" + + "github.com/grafana/grafana/pkg/infra/log" ) type rulesConfigReader struct { diff --git a/pkg/services/provisioning/alerting/config_reader_test.go b/pkg/services/provisioning/alerting/config_reader_test.go index f17cc3a8b61..edcf431e690 100644 --- a/pkg/services/provisioning/alerting/config_reader_test.go +++ b/pkg/services/provisioning/alerting/config_reader_test.go @@ -4,8 +4,9 @@ import ( "context" "testing" - "github.com/grafana/grafana/pkg/infra/log" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/infra/log" ) const ( diff --git a/pkg/services/provisioning/alerting/contact_point_types_test.go b/pkg/services/provisioning/alerting/contact_point_types_test.go index 34141f0949e..57ef9acfca0 100644 --- a/pkg/services/provisioning/alerting/contact_point_types_test.go +++ b/pkg/services/provisioning/alerting/contact_point_types_test.go @@ -3,9 +3,10 @@ package alerting import ( "testing" - "github.com/grafana/grafana/pkg/services/provisioning/values" "github.com/stretchr/testify/require" "gopkg.in/yaml.v3" + + "github.com/grafana/grafana/pkg/services/provisioning/values" ) func TestReceivers(t *testing.T) { diff --git a/pkg/services/provisioning/alerting/file/rules_types.go b/pkg/services/provisioning/alerting/file/rules_types.go new file mode 100644 index 00000000000..59782b08c71 --- /dev/null +++ b/pkg/services/provisioning/alerting/file/rules_types.go @@ -0,0 +1,306 @@ +package file + +import ( + "encoding/json" + "errors" + "fmt" + "strings" + "time" + + "github.com/prometheus/common/model" + + "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/services/provisioning/values" +) + +type RuleDelete struct { + UID string + OrgID int64 +} + +type RuleDeleteV1 struct { + UID values.StringValue `json:"uid" yaml:"uid"` + OrgID values.Int64Value `json:"orgId" yaml:"orgId"` +} + +type AlertRuleGroupV1 struct { + OrgID values.Int64Value `json:"orgId" yaml:"orgId"` + Name values.StringValue `json:"name" yaml:"name"` + Folder values.StringValue `json:"folder" yaml:"folder"` + Interval values.StringValue `json:"interval" yaml:"interval"` + Rules []AlertRuleV1 `json:"rules" yaml:"rules"` +} + +func (ruleGroupV1 *AlertRuleGroupV1) MapToModel() (AlertRuleGroupWithFolderTitle, error) { + ruleGroup := AlertRuleGroupWithFolderTitle{AlertRuleGroup: &models.AlertRuleGroup{}} + ruleGroup.Title = ruleGroupV1.Name.Value() + if strings.TrimSpace(ruleGroup.Title) == "" { + return AlertRuleGroupWithFolderTitle{}, errors.New("rule group has no name set") + } + ruleGroup.OrgID = ruleGroupV1.OrgID.Value() + if ruleGroup.OrgID < 1 { + ruleGroup.OrgID = 1 + } + interval, err := model.ParseDuration(ruleGroupV1.Interval.Value()) + if err != nil { + return AlertRuleGroupWithFolderTitle{}, err + } + ruleGroup.Interval = int64(time.Duration(interval).Seconds()) + ruleGroup.FolderTitle = ruleGroupV1.Folder.Value() + if strings.TrimSpace(ruleGroup.FolderTitle) == "" { + return AlertRuleGroupWithFolderTitle{}, errors.New("rule group has no folder set") + } + for _, ruleV1 := range ruleGroupV1.Rules { + rule, err := ruleV1.mapToModel(ruleGroup.OrgID) + if err != nil { + return AlertRuleGroupWithFolderTitle{}, err + } + ruleGroup.Rules = append(ruleGroup.Rules, rule) + } + return ruleGroup, nil +} + +type AlertRuleGroupWithFolderTitle struct { + *models.AlertRuleGroup + OrgID int64 + FolderTitle string +} + +type AlertRuleV1 struct { + UID values.StringValue `json:"uid" yaml:"uid"` + Title values.StringValue `json:"title" yaml:"title"` + Condition values.StringValue `json:"condition" yaml:"condition"` + Data []QueryV1 `json:"data" yaml:"data"` + DashboardUID values.StringValue `json:"dasboardUid" yaml:"dashboardUid"` + PanelID values.Int64Value `json:"panelId" yaml:"panelId"` + NoDataState values.StringValue `json:"noDataState" yaml:"noDataState"` + ExecErrState values.StringValue `json:"execErrState" yaml:"execErrState"` + For values.StringValue `json:"for" yaml:"for"` + Annotations values.StringMapValue `json:"annotations" yaml:"annotations"` + Labels values.StringMapValue `json:"labels" yaml:"labels"` + IsPaused values.BoolValue `json:"isPaused" yaml:"isPaused"` +} + +func (rule *AlertRuleV1) mapToModel(orgID int64) (models.AlertRule, error) { + alertRule := models.AlertRule{} + alertRule.Title = rule.Title.Value() + if alertRule.Title == "" { + return models.AlertRule{}, fmt.Errorf("rule has no title set") + } + alertRule.UID = rule.UID.Value() + if alertRule.UID == "" { + return models.AlertRule{}, fmt.Errorf("rule '%s' failed to parse: no UID set", alertRule.Title) + } + alertRule.OrgID = orgID + duration, err := model.ParseDuration(rule.For.Value()) + if err != nil { + return models.AlertRule{}, fmt.Errorf("rule '%s' failed to parse: %w", alertRule.Title, err) + } + alertRule.For = time.Duration(duration) + dashboardUID := rule.DashboardUID.Value() + alertRule.DashboardUID = &dashboardUID + panelID := rule.PanelID.Value() + alertRule.PanelID = &panelID + execErrStateValue := strings.TrimSpace(rule.ExecErrState.Value()) + execErrState, err := models.ErrStateFromString(execErrStateValue) + if err != nil && execErrStateValue != "" { + return models.AlertRule{}, fmt.Errorf("rule '%s' failed to parse: %w", alertRule.Title, err) + } + if execErrStateValue == "" { + execErrState = models.AlertingErrState + } + alertRule.ExecErrState = execErrState + noDataStateValue := strings.TrimSpace(rule.NoDataState.Value()) + noDataState, err := models.NoDataStateFromString(noDataStateValue) + if err != nil && noDataStateValue != "" { + return models.AlertRule{}, fmt.Errorf("rule '%s' failed to parse: %w", alertRule.Title, err) + } + if noDataStateValue == "" { + noDataState = models.NoData + } + alertRule.NoDataState = noDataState + alertRule.Condition = rule.Condition.Value() + if alertRule.Condition == "" { + return models.AlertRule{}, fmt.Errorf("rule '%s' failed to parse: no condition set", alertRule.Title) + } + alertRule.Annotations = rule.Annotations.Raw + alertRule.Labels = rule.Labels.Value() + for _, queryV1 := range rule.Data { + query, err := queryV1.mapToModel() + if err != nil { + return models.AlertRule{}, fmt.Errorf("rule '%s' failed to parse: %w", alertRule.Title, err) + } + alertRule.Data = append(alertRule.Data, query) + } + if len(alertRule.Data) == 0 { + return models.AlertRule{}, fmt.Errorf("rule '%s' failed to parse: no data set", alertRule.Title) + } + alertRule.IsPaused = rule.IsPaused.Value() + return alertRule, nil +} + +type QueryV1 struct { + RefID values.StringValue `json:"refId" yaml:"refId"` + QueryType values.StringValue `json:"queryType" yaml:"queryType"` + RelativeTimeRange models.RelativeTimeRange `json:"relativeTimeRange" yaml:"relativeTimeRange"` + DatasourceUID values.StringValue `json:"datasourceUid" yaml:"datasourceUid"` + Model values.JSONValue `json:"model" yaml:"model"` +} + +func (queryV1 *QueryV1) mapToModel() (models.AlertQuery, error) { + // In order to get the model into the format we need, + // we marshal it back to json and unmarshal it again + // in json.RawMessage. We do this as we cannot use + // json.RawMessage with a yaml files and have to use + // JSONValue that supports both, json and yaml. + // + // We have to use the Raw field here, as Value would + // try to interpolate macros like `$__timeFilter`, resulting + // in missing macros in the SQL queries as they would be + // replaced by an empty string. + encoded, err := json.Marshal(queryV1.Model.Raw) + if err != nil { + return models.AlertQuery{}, err + } + var rawMessage json.RawMessage + err = json.Unmarshal(encoded, &rawMessage) + if err != nil { + return models.AlertQuery{}, err + } + return models.AlertQuery{ + RefID: queryV1.RefID.Value(), + QueryType: queryV1.QueryType.Value(), + DatasourceUID: queryV1.DatasourceUID.Value(), + RelativeTimeRange: queryV1.RelativeTimeRange, + Model: rawMessage, + }, nil +} + +// Response structs + +// AlertingFileExport is the full provisioned file export. +// swagger:model +type AlertingFileExport struct { + APIVersion int64 `json:"apiVersion" yaml:"apiVersion"` + Groups []AlertRuleGroupExport `json:"groups" yaml:"groups"` +} + +// AlertRuleGroupExport is the provisioned file export of AlertRuleGroupV1. +type AlertRuleGroupExport struct { + OrgID int64 `json:"orgId" yaml:"orgId"` + Name string `json:"name" yaml:"name"` + Folder string `json:"folder" yaml:"folder"` + Interval model.Duration `json:"interval" yaml:"interval"` + Rules []AlertRuleExport `json:"rules" yaml:"rules"` +} + +// AlertRuleExport is the provisioned file export of models.AlertRule. +type AlertRuleExport struct { + UID string `json:"uid" yaml:"uid"` + Title string `json:"title" yaml:"title"` + Condition string `json:"condition" yaml:"condition"` + Data []AlertQueryExport `json:"data" yaml:"data"` + DashboardUID string `json:"dasboardUid,omitempty" yaml:"dashboardUid,omitempty"` + PanelID int64 `json:"panelId,omitempty" yaml:"panelId,omitempty"` + NoDataState models.NoDataState `json:"noDataState" yaml:"noDataState"` + ExecErrState models.ExecutionErrorState `json:"execErrState" yaml:"execErrState"` + For model.Duration `json:"for" yaml:"for"` + Annotations map[string]string `json:"annotations,omitempty" yaml:"annotations,omitempty"` + Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"` + IsPaused bool `json:"isPaused" yaml:"isPaused"` +} + +// AlertQueryExport is the provisioned export of models.AlertQuery. +type AlertQueryExport struct { + RefID string `json:"refId" yaml:"refId"` + QueryType string `json:"queryType,omitempty" yaml:"queryType,omitempty"` + RelativeTimeRange models.RelativeTimeRange `json:"relativeTimeRange,omitempty" yaml:"relativeTimeRange,omitempty"` + DatasourceUID string `json:"datasourceUid" yaml:"datasourceUid"` + Model map[string]interface{} `json:"model" yaml:"model"` +} + +// NewAlertingFileExport creates an AlertingFileExport DTO from []AlertRuleGroupWithFolderTitle. +func NewAlertingFileExport(groups []AlertRuleGroupWithFolderTitle) (AlertingFileExport, error) { + f := AlertingFileExport{APIVersion: 1} + for _, group := range groups { + export, err := newAlertRuleGroupExport(group) + if err != nil { + return AlertingFileExport{}, err + } + f.Groups = append(f.Groups, export) + } + return f, nil +} + +// newAlertRuleGroupExport creates a AlertRuleGroupExport DTO from models.AlertRuleGroup. +func newAlertRuleGroupExport(d AlertRuleGroupWithFolderTitle) (AlertRuleGroupExport, error) { + rules := make([]AlertRuleExport, 0, len(d.Rules)) + for i := range d.Rules { + alert, err := newAlertRuleExport(d.Rules[i]) + if err != nil { + return AlertRuleGroupExport{}, err + } + rules = append(rules, alert) + } + return AlertRuleGroupExport{ + OrgID: d.OrgID, + Name: d.Title, + Folder: d.FolderTitle, + Interval: model.Duration(time.Duration(d.Interval) * time.Second), + Rules: rules, + }, nil +} + +// newAlertRuleExport creates a AlertRuleExport DTO from models.AlertRule. +func newAlertRuleExport(rule models.AlertRule) (AlertRuleExport, error) { + data := make([]AlertQueryExport, 0, len(rule.Data)) + for i := range rule.Data { + query, err := newAlertQueryExport(rule.Data[i]) + if err != nil { + return AlertRuleExport{}, err + } + data = append(data, query) + } + + var dashboardUID string + if rule.DashboardUID != nil { + dashboardUID = *rule.DashboardUID + } + + var panelID int64 + if rule.PanelID != nil { + panelID = *rule.PanelID + } + + return AlertRuleExport{ + UID: rule.UID, + Title: rule.Title, + For: model.Duration(rule.For), + Condition: rule.Condition, + Data: data, + DashboardUID: dashboardUID, + PanelID: panelID, + NoDataState: rule.NoDataState, + ExecErrState: rule.ExecErrState, + Annotations: rule.Annotations, + Labels: rule.Labels, + IsPaused: rule.IsPaused, + }, nil +} + +// newAlertQueryExport creates a AlertQueryExport DTO from models.AlertQuery. +func newAlertQueryExport(query models.AlertQuery) (AlertQueryExport, error) { + // We unmarshal the json.RawMessage model into a map in order to facilitate yaml marshalling. + var mdl map[string]interface{} + err := json.Unmarshal(query.Model, &mdl) + if err != nil { + return AlertQueryExport{}, err + } + return AlertQueryExport{ + RefID: query.RefID, + QueryType: query.QueryType, + RelativeTimeRange: query.RelativeTimeRange, + DatasourceUID: query.DatasourceUID, + Model: mdl, + }, nil +} diff --git a/pkg/services/provisioning/alerting/rules_types_test.go b/pkg/services/provisioning/alerting/file/rules_types_test.go similarity index 98% rename from pkg/services/provisioning/alerting/rules_types_test.go rename to pkg/services/provisioning/alerting/file/rules_types_test.go index 0eac93e2404..abc2836f01a 100644 --- a/pkg/services/provisioning/alerting/rules_types_test.go +++ b/pkg/services/provisioning/alerting/file/rules_types_test.go @@ -1,13 +1,14 @@ -package alerting +package file import ( "testing" "time" - "github.com/grafana/grafana/pkg/services/ngalert/models" - "github.com/grafana/grafana/pkg/services/provisioning/values" "github.com/stretchr/testify/require" "gopkg.in/yaml.v3" + + "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/services/provisioning/values" ) func TestRuleGroup(t *testing.T) { @@ -60,7 +61,7 @@ func TestRuleGroup(t *testing.T) { rg.Interval = interval rgMapped, err := rg.MapToModel() require.NoError(t, err) - require.Equal(t, 48*time.Hour, rgMapped.Interval) + require.Equal(t, int64(48*time.Hour/time.Second), rgMapped.Interval) }) t.Run("a rule group with an empty org id should default to 1", func(t *testing.T) { rg := validRuleGroupV1(t) diff --git a/pkg/services/provisioning/alerting/notification_policy_types_test.go b/pkg/services/provisioning/alerting/notification_policy_types_test.go index d281427e5a6..15080a154f3 100644 --- a/pkg/services/provisioning/alerting/notification_policy_types_test.go +++ b/pkg/services/provisioning/alerting/notification_policy_types_test.go @@ -4,9 +4,8 @@ import ( "os" "testing" - "gopkg.in/yaml.v3" - "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" ) func TestNotificationPolicy(t *testing.T) { diff --git a/pkg/services/provisioning/alerting/rules_provisioner.go b/pkg/services/provisioning/alerting/rules_provisioner.go index 55750ff0421..36fdbef76ea 100644 --- a/pkg/services/provisioning/alerting/rules_provisioner.go +++ b/pkg/services/provisioning/alerting/rules_provisioner.go @@ -41,24 +41,24 @@ func (prov *defaultAlertRuleProvisioner) Provision(ctx context.Context, files []*AlertingFile) error { for _, file := range files { for _, group := range file.Groups { - folderUID, err := prov.getOrCreateFolderUID(ctx, group.Folder, group.OrgID) + folderUID, err := prov.getOrCreateFolderUID(ctx, group.FolderTitle, group.OrgID) if err != nil { return err } prov.logger.Debug("provisioning alert rule group", "org", group.OrgID, - "folder", group.Folder, + "folder", group.FolderTitle, "folderUID", folderUID, - "name", group.Name) + "name", group.Title) for _, rule := range group.Rules { rule.NamespaceUID = folderUID - rule.RuleGroup = group.Name - err = prov.provisionRule(ctx, group.OrgID, rule, group.Folder, folderUID) + rule.RuleGroup = group.Title + err = prov.provisionRule(ctx, group.OrgID, rule) if err != nil { return err } } - err = prov.ruleService.UpdateRuleGroup(ctx, group.OrgID, folderUID, group.Name, int64(group.Interval.Seconds())) + err = prov.ruleService.UpdateRuleGroup(ctx, group.OrgID, folderUID, group.Title, group.Interval) if err != nil { return err } @@ -77,9 +77,7 @@ func (prov *defaultAlertRuleProvisioner) Provision(ctx context.Context, func (prov *defaultAlertRuleProvisioner) provisionRule( ctx context.Context, orgID int64, - rule alert_models.AlertRule, - folder, - folderUID string) error { + rule alert_models.AlertRule) error { prov.logger.Debug("provisioning alert rule", "uid", rule.UID, "org", rule.OrgID) _, _, err := prov.ruleService.GetAlertRule(ctx, orgID, rule.UID) if err != nil && !errors.Is(err, alert_models.ErrAlertRuleNotFound) { diff --git a/pkg/services/provisioning/alerting/rules_types.go b/pkg/services/provisioning/alerting/rules_types.go deleted file mode 100644 index b300d918c46..00000000000 --- a/pkg/services/provisioning/alerting/rules_types.go +++ /dev/null @@ -1,177 +0,0 @@ -package alerting - -import ( - "encoding/json" - "errors" - "fmt" - "strings" - "time" - - "github.com/grafana/grafana/pkg/services/ngalert/models" - "github.com/grafana/grafana/pkg/services/provisioning/values" - - "github.com/prometheus/common/model" -) - -type RuleDelete struct { - UID string - OrgID int64 -} - -type RuleDeleteV1 struct { - UID values.StringValue `json:"uid" yaml:"uid"` - OrgID values.Int64Value `json:"orgId" yaml:"orgId"` -} - -type AlertRuleGroupV1 struct { - OrgID values.Int64Value `json:"orgId" yaml:"orgId"` - Name values.StringValue `json:"name" yaml:"name"` - Folder values.StringValue `json:"folder" yaml:"folder"` - Interval values.StringValue `json:"interval" yaml:"interval"` - Rules []AlertRuleV1 `json:"rules" yaml:"rules"` -} - -func (ruleGroupV1 *AlertRuleGroupV1) MapToModel() (AlertRuleGroup, error) { - ruleGroup := AlertRuleGroup{} - ruleGroup.Name = ruleGroupV1.Name.Value() - if strings.TrimSpace(ruleGroup.Name) == "" { - return AlertRuleGroup{}, errors.New("rule group has no name set") - } - ruleGroup.OrgID = ruleGroupV1.OrgID.Value() - if ruleGroup.OrgID < 1 { - ruleGroup.OrgID = 1 - } - interval, err := model.ParseDuration(ruleGroupV1.Interval.Value()) - if err != nil { - return AlertRuleGroup{}, err - } - ruleGroup.Interval = time.Duration(interval) - ruleGroup.Folder = ruleGroupV1.Folder.Value() - if strings.TrimSpace(ruleGroup.Folder) == "" { - return AlertRuleGroup{}, errors.New("rule group has no folder set") - } - for _, ruleV1 := range ruleGroupV1.Rules { - rule, err := ruleV1.mapToModel(ruleGroup.OrgID) - if err != nil { - return AlertRuleGroup{}, err - } - ruleGroup.Rules = append(ruleGroup.Rules, rule) - } - return ruleGroup, nil -} - -type AlertRuleGroup struct { - OrgID int64 - Name string - Folder string - Interval time.Duration - Rules []models.AlertRule -} - -type AlertRuleV1 struct { - UID values.StringValue `json:"uid" yaml:"uid"` - Title values.StringValue `json:"title" yaml:"title"` - Condition values.StringValue `json:"condition" yaml:"condition"` - Data []QueryV1 `json:"data" yaml:"data"` - DashboardUID values.StringValue `json:"dasboardUid" yaml:"dashboardUid"` - PanelID values.Int64Value `json:"panelId" yaml:"panelId"` - NoDataState values.StringValue `json:"noDataState" yaml:"noDataState"` - ExecErrState values.StringValue `json:"execErrState" yaml:"execErrState"` - For values.StringValue `json:"for" yaml:"for"` - Annotations values.StringMapValue `json:"annotations" yaml:"annotations"` - Labels values.StringMapValue `json:"labels" yaml:"labels"` -} - -func (rule *AlertRuleV1) mapToModel(orgID int64) (models.AlertRule, error) { - alertRule := models.AlertRule{} - alertRule.Title = rule.Title.Value() - if alertRule.Title == "" { - return models.AlertRule{}, fmt.Errorf("rule has no title set") - } - alertRule.UID = rule.UID.Value() - if alertRule.UID == "" { - return models.AlertRule{}, fmt.Errorf("rule '%s' failed to parse: no UID set", alertRule.Title) - } - alertRule.OrgID = orgID - duration, err := model.ParseDuration(rule.For.Value()) - if err != nil { - return models.AlertRule{}, fmt.Errorf("rule '%s' failed to parse: %w", alertRule.Title, err) - } - alertRule.For = time.Duration(duration) - dashboardUID := rule.DashboardUID.Value() - alertRule.DashboardUID = &dashboardUID - panelID := rule.PanelID.Value() - alertRule.PanelID = &panelID - execErrStateValue := strings.TrimSpace(rule.ExecErrState.Value()) - execErrState, err := models.ErrStateFromString(execErrStateValue) - if err != nil && execErrStateValue != "" { - return models.AlertRule{}, fmt.Errorf("rule '%s' failed to parse: %w", alertRule.Title, err) - } - if execErrStateValue == "" { - execErrState = models.AlertingErrState - } - alertRule.ExecErrState = execErrState - noDataStateValue := strings.TrimSpace(rule.NoDataState.Value()) - noDataState, err := models.NoDataStateFromString(noDataStateValue) - if err != nil && noDataStateValue != "" { - return models.AlertRule{}, fmt.Errorf("rule '%s' failed to parse: %w", alertRule.Title, err) - } - if noDataStateValue == "" { - noDataState = models.NoData - } - alertRule.NoDataState = noDataState - alertRule.Condition = rule.Condition.Value() - if alertRule.Condition == "" { - return models.AlertRule{}, fmt.Errorf("rule '%s' failed to parse: no condition set", alertRule.Title) - } - alertRule.Annotations = rule.Annotations.Raw - alertRule.Labels = rule.Labels.Value() - for _, queryV1 := range rule.Data { - query, err := queryV1.mapToModel() - if err != nil { - return models.AlertRule{}, fmt.Errorf("rule '%s' failed to parse: %w", alertRule.Title, err) - } - alertRule.Data = append(alertRule.Data, query) - } - if len(alertRule.Data) == 0 { - return models.AlertRule{}, fmt.Errorf("rule '%s' failed to parse: no data set", alertRule.Title) - } - return alertRule, nil -} - -type QueryV1 struct { - RefID values.StringValue `json:"refId" yaml:"refId"` - QueryType values.StringValue `json:"queryType" yaml:"queryType"` - RelativeTimeRange models.RelativeTimeRange `json:"relativeTimeRange" yaml:"relativeTimeRange"` - DatasourceUID values.StringValue `json:"datasourceUid" yaml:"datasourceUid"` - Model values.JSONValue `json:"model" yaml:"model"` -} - -func (queryV1 *QueryV1) mapToModel() (models.AlertQuery, error) { - // In order to get the model into the format we need, - // we marshal it back to json and unmarshal it again - // in json.RawMessage. We do this as we cannot use - // json.RawMessage with a yaml files and have to use - // JSONValue that supports both, json and yaml. - // - // We have to use the Raw field here, as Value would - // try to interpolate macros like `$__timeFilter`, resulting - // in missing macros in the SQL queries as they would be - // replaced by an empty string. - encoded, err := json.Marshal(queryV1.Model.Raw) - if err != nil { - return models.AlertQuery{}, err - } - var rawMessage json.RawMessage - err = json.Unmarshal(encoded, &rawMessage) - if err != nil { - return models.AlertQuery{}, err - } - return models.AlertQuery{ - RefID: queryV1.RefID.Value(), - QueryType: queryV1.QueryType.Value(), - DatasourceUID: queryV1.DatasourceUID.Value(), - RelativeTimeRange: queryV1.RelativeTimeRange, - Model: rawMessage, - }, nil -} diff --git a/pkg/services/provisioning/alerting/types.go b/pkg/services/provisioning/alerting/types.go index 6b500c85dd1..6a3331c095b 100644 --- a/pkg/services/provisioning/alerting/types.go +++ b/pkg/services/provisioning/alerting/types.go @@ -3,6 +3,7 @@ package alerting import ( "fmt" + "github.com/grafana/grafana/pkg/services/provisioning/alerting/file" "github.com/grafana/grafana/pkg/services/provisioning/values" ) @@ -15,8 +16,8 @@ type OrgID int64 type AlertingFile struct { configVersion Filename string - Groups []AlertRuleGroup - DeleteRules []RuleDelete + Groups []file.AlertRuleGroupWithFolderTitle + DeleteRules []file.RuleDelete ContactPoints []ContactPoint DeleteContactPoints []DeleteContactPoint Policies []NotificiationPolicy @@ -30,8 +31,8 @@ type AlertingFile struct { type AlertingFileV1 struct { configVersion Filename string - Groups []AlertRuleGroupV1 `json:"groups" yaml:"groups"` - DeleteRules []RuleDeleteV1 `json:"deleteRules" yaml:"deleteRules"` + Groups []file.AlertRuleGroupV1 `json:"groups" yaml:"groups"` + DeleteRules []file.RuleDeleteV1 `json:"deleteRules" yaml:"deleteRules"` ContactPoints []ContactPointV1 `json:"contactPoints" yaml:"contactPoints"` DeleteContactPoints []DeleteContactPointV1 `json:"deleteContactPoints" yaml:"deleteContactPoints"` Policies []NotificiationPolicyV1 `json:"policies" yaml:"policies"` @@ -132,7 +133,7 @@ func (fileV1 *AlertingFileV1) mapRules(alertingFile *AlertingFile) error { if orgID < 1 { orgID = 1 } - ruleDelete := RuleDelete{ + ruleDelete := file.RuleDelete{ UID: ruleDeleteV1.UID.Value(), OrgID: orgID, } diff --git a/pkg/services/provisioning/dashboards/config_reader.go b/pkg/services/provisioning/dashboards/config_reader.go index a8aa1a06b3b..8d93c4de46b 100644 --- a/pkg/services/provisioning/dashboards/config_reader.go +++ b/pkg/services/provisioning/dashboards/config_reader.go @@ -8,10 +8,11 @@ import ( "path/filepath" "strings" + "gopkg.in/yaml.v3" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/provisioning/utils" - "gopkg.in/yaml.v3" ) type configReader struct { diff --git a/pkg/services/provisioning/dashboards/file_reader_symlink_test.go b/pkg/services/provisioning/dashboards/file_reader_symlink_test.go index 91b356e251a..b72425ea1c5 100644 --- a/pkg/services/provisioning/dashboards/file_reader_symlink_test.go +++ b/pkg/services/provisioning/dashboards/file_reader_symlink_test.go @@ -7,9 +7,10 @@ import ( "path/filepath" "testing" - "github.com/grafana/grafana/pkg/infra/log" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/infra/log" ) var ( diff --git a/pkg/services/provisioning/plugins/config_reader_test.go b/pkg/services/provisioning/plugins/config_reader_test.go index 497cca2c11c..3440c5848d7 100644 --- a/pkg/services/provisioning/plugins/config_reader_test.go +++ b/pkg/services/provisioning/plugins/config_reader_test.go @@ -5,9 +5,10 @@ import ( "os" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/plugins" - "github.com/stretchr/testify/require" ) const ( diff --git a/pkg/services/provisioning/provisioning.go b/pkg/services/provisioning/provisioning.go index fe6f7c727fa..9f7bff61917 100644 --- a/pkg/services/provisioning/provisioning.go +++ b/pkg/services/provisioning/provisioning.go @@ -269,6 +269,7 @@ func (ps *ProvisioningServiceImpl) ProvisionAlerting(ctx context.Context) error ruleService := provisioning.NewAlertRuleService( st, st, + ps.dashboardService, ps.quotaService, ps.SQLStore, int64(ps.Cfg.UnifiedAlerting.DefaultRuleEvaluationInterval.Seconds()), diff --git a/pkg/services/provisioning/provisioning_test.go b/pkg/services/provisioning/provisioning_test.go index 4b18fab3310..e7626af9d37 100644 --- a/pkg/services/provisioning/provisioning_test.go +++ b/pkg/services/provisioning/provisioning_test.go @@ -6,12 +6,13 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + dashboardstore "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/provisioning/dashboards" "github.com/grafana/grafana/pkg/services/provisioning/utils" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/assert" ) func TestProvisioningServiceImpl(t *testing.T) { diff --git a/pkg/services/provisioning/values/values_test.go b/pkg/services/provisioning/values/values_test.go index 15954efd401..7ee470ec8ff 100644 --- a/pkg/services/provisioning/values/values_test.go +++ b/pkg/services/provisioning/values/values_test.go @@ -6,11 +6,12 @@ import ( "os" "testing" - "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gopkg.in/ini.v1" "gopkg.in/yaml.v3" + + "github.com/grafana/grafana/pkg/setting" ) func TestValues(t *testing.T) { diff --git a/pkg/services/publicdashboards/api/api.go b/pkg/services/publicdashboards/api/api.go index 1837911e62d..8ac11743ed6 100644 --- a/pkg/services/publicdashboards/api/api.go +++ b/pkg/services/publicdashboards/api/api.go @@ -8,13 +8,13 @@ import ( "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/publicdashboards" - "github.com/grafana/grafana/pkg/services/publicdashboards/internal/tokens" . "github.com/grafana/grafana/pkg/services/publicdashboards/models" + "github.com/grafana/grafana/pkg/services/publicdashboards/validation" "github.com/grafana/grafana/pkg/web" ) @@ -89,7 +89,7 @@ func (api *Api) RegisterAPIEndpoints() { // ListPublicDashboards Gets list of public dashboards by orgId // GET /api/dashboards/public-dashboards -func (api *Api) ListPublicDashboards(c *models.ReqContext) response.Response { +func (api *Api) ListPublicDashboards(c *contextmodel.ReqContext) response.Response { resp, err := api.PublicDashboardService.FindAll(c.Req.Context(), c.SignedInUser, c.OrgID) if err != nil { return response.Err(err) @@ -99,10 +99,10 @@ func (api *Api) ListPublicDashboards(c *models.ReqContext) response.Response { // GetPublicDashboard Gets public dashboard for dashboard // GET /api/dashboards/uid/:dashboardUid/public-dashboards -func (api *Api) GetPublicDashboard(c *models.ReqContext) response.Response { +func (api *Api) GetPublicDashboard(c *contextmodel.ReqContext) response.Response { // exit if we don't have a valid dashboardUid dashboardUid := web.Params(c.Req)[":dashboardUid"] - if !tokens.IsValidShortUID(dashboardUid) { + if !validation.IsValidShortUID(dashboardUid) { return response.Err(ErrPublicDashboardIdentifierNotSet.Errorf("GetPublicDashboard: no dashboard Uid for public dashboard specified")) } @@ -120,10 +120,10 @@ func (api *Api) GetPublicDashboard(c *models.ReqContext) response.Response { // CreatePublicDashboard Sets public dashboard for dashboard // POST /api/dashboards/uid/:dashboardUid/public-dashboards -func (api *Api) CreatePublicDashboard(c *models.ReqContext) response.Response { +func (api *Api) CreatePublicDashboard(c *contextmodel.ReqContext) response.Response { // exit if we don't have a valid dashboardUid dashboardUid := web.Params(c.Req)[":dashboardUid"] - if !tokens.IsValidShortUID(dashboardUid) { + if !validation.IsValidShortUID(dashboardUid) { return response.Err(ErrInvalidUid.Errorf("CreatePublicDashboard: invalid Uid %s", dashboardUid)) } @@ -152,15 +152,15 @@ func (api *Api) CreatePublicDashboard(c *models.ReqContext) response.Response { // UpdatePublicDashboard Sets public dashboard for dashboard // PUT /api/dashboards/uid/:dashboardUid/public-dashboards/:uid -func (api *Api) UpdatePublicDashboard(c *models.ReqContext) response.Response { +func (api *Api) UpdatePublicDashboard(c *contextmodel.ReqContext) response.Response { // exit if we don't have a valid dashboardUid dashboardUid := web.Params(c.Req)[":dashboardUid"] - if !tokens.IsValidShortUID(dashboardUid) { + if !validation.IsValidShortUID(dashboardUid) { return response.Err(ErrInvalidUid.Errorf("UpdatePublicDashboard: invalid dashboard Uid %s", dashboardUid)) } uid := web.Params(c.Req)[":uid"] - if !tokens.IsValidShortUID(uid) { + if !validation.IsValidShortUID(uid) { return response.Err(ErrInvalidUid.Errorf("UpdatePublicDashboard: invalid Uid %s", uid)) } @@ -190,9 +190,9 @@ func (api *Api) UpdatePublicDashboard(c *models.ReqContext) response.Response { // Delete a public dashboard // DELETE /api/dashboards/uid/:dashboardUid/public-dashboards/:uid -func (api *Api) DeletePublicDashboard(c *models.ReqContext) response.Response { +func (api *Api) DeletePublicDashboard(c *contextmodel.ReqContext) response.Response { uid := web.Params(c.Req)[":uid"] - if !tokens.IsValidShortUID(uid) { + if !validation.IsValidShortUID(uid) { return response.Err(ErrInvalidUid.Errorf("UpdatePublicDashboard: invalid Uid %s", uid)) } diff --git a/pkg/services/publicdashboards/api/common_test.go b/pkg/services/publicdashboards/api/common_test.go index a2cf1bb6238..47a96d39363 100644 --- a/pkg/services/publicdashboards/api/common_test.go +++ b/pkg/services/publicdashboards/api/common_test.go @@ -15,12 +15,12 @@ import ( "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/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" fakeDatasources "github.com/grafana/grafana/pkg/services/datasources/fakes" datasourceService "github.com/grafana/grafana/pkg/services/datasources/service" @@ -82,7 +82,7 @@ type testContext struct { func contextProvider(tc *testContext) web.Handler { return func(c *web.Context) { signedIn := tc.user != nil - reqCtx := &models.ReqContext{ + reqCtx := &contextmodel.ReqContext{ Context: c, SignedInUser: tc.user, IsSignedIn: signedIn, diff --git a/pkg/services/publicdashboards/api/middleware.go b/pkg/services/publicdashboards/api/middleware.go index 856bc2899ad..1805ae9c358 100644 --- a/pkg/services/publicdashboards/api/middleware.go +++ b/pkg/services/publicdashboards/api/middleware.go @@ -4,17 +4,17 @@ import ( "net/http" "github.com/grafana/grafana/pkg/infra/metrics" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/publicdashboards" - "github.com/grafana/grafana/pkg/services/publicdashboards/internal/tokens" + "github.com/grafana/grafana/pkg/services/publicdashboards/validation" "github.com/grafana/grafana/pkg/web" ) // SetPublicDashboardOrgIdOnContext Adds orgId to context based on org of public dashboard -func SetPublicDashboardOrgIdOnContext(publicDashboardService publicdashboards.Service) func(c *models.ReqContext) { - return func(c *models.ReqContext) { +func SetPublicDashboardOrgIdOnContext(publicDashboardService publicdashboards.Service) func(c *contextmodel.ReqContext) { + return func(c *contextmodel.ReqContext) { accessToken, ok := web.Params(c.Req)[":accessToken"] - if !ok || !tokens.IsValidAccessToken(accessToken) { + if !ok || !validation.IsValidAccessToken(accessToken) { return } @@ -29,15 +29,15 @@ func SetPublicDashboardOrgIdOnContext(publicDashboardService publicdashboards.Se } // SetPublicDashboardFlag Adds public dashboard flag on context -func SetPublicDashboardFlag(c *models.ReqContext) { +func SetPublicDashboardFlag(c *contextmodel.ReqContext) { c.IsPublicDashboardView = true } // RequiresExistingAccessToken Middleware to enforce that a public dashboards exists before continuing to handler. This // method will query the database to ensure that it exists. // Use when we want to enforce a public dashboard is valid on an endpoint we do not maintain -func RequiresExistingAccessToken(publicDashboardService publicdashboards.Service) func(c *models.ReqContext) { - return func(c *models.ReqContext) { +func RequiresExistingAccessToken(publicDashboardService publicdashboards.Service) func(c *contextmodel.ReqContext) { + return func(c *contextmodel.ReqContext) { accessToken, ok := web.Params(c.Req)[":accessToken"] if !ok { @@ -45,7 +45,7 @@ func RequiresExistingAccessToken(publicDashboardService publicdashboards.Service return } - if !tokens.IsValidAccessToken(accessToken) { + if !validation.IsValidAccessToken(accessToken) { c.JsonApiErr(http.StatusBadRequest, "Invalid access token", nil) } @@ -62,8 +62,8 @@ func RequiresExistingAccessToken(publicDashboardService publicdashboards.Service } } -func CountPublicDashboardRequest() func(c *models.ReqContext) { - return func(c *models.ReqContext) { +func CountPublicDashboardRequest() func(c *contextmodel.ReqContext) { + return func(c *contextmodel.ReqContext) { metrics.MPublicDashboardRequestCount.Inc() } } diff --git a/pkg/services/publicdashboards/api/middleware_test.go b/pkg/services/publicdashboards/api/middleware_test.go index 20680679ab5..3e6f9811705 100644 --- a/pkg/services/publicdashboards/api/middleware_test.go +++ b/pkg/services/publicdashboards/api/middleware_test.go @@ -8,9 +8,9 @@ import ( "errors" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/publicdashboards" - "github.com/grafana/grafana/pkg/services/publicdashboards/internal/tokens" + "github.com/grafana/grafana/pkg/services/publicdashboards/service" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/web" "github.com/stretchr/testify/assert" @@ -18,7 +18,7 @@ import ( "github.com/stretchr/testify/require" ) -var validAccessToken, _ = tokens.GenerateAccessToken() +var validAccessToken, _ = service.GenerateAccessToken() func TestRequiresExistingAccessToken(t *testing.T) { tests := []struct { @@ -146,21 +146,21 @@ func TestSetPublicDashboardOrgIdOnContext(t *testing.T) { func TestSetPublicDashboardFlag(t *testing.T) { t.Run("Adds context.IsPublicDashboardView=true to request", func(t *testing.T) { - ctx := &models.ReqContext{} + ctx := &contextmodel.ReqContext{} SetPublicDashboardFlag(ctx) assert.True(t, ctx.IsPublicDashboardView) }) } // This is a helper to test middleware. It handles creating a -// proper models.ReqContext, setting web parameters, executing middleware, and +// proper contextmodel.ReqContext, setting web parameters, executing middleware, and // returning a response. Response will default to result of // httptest.NewRecorder() return value and will only change if modified by the // middlware as this will no accept a handler method -func runMw(t *testing.T, ctx *models.ReqContext, httpmethod string, path string, webparams map[string]string, mw func(c *models.ReqContext)) (*models.ReqContext, *httptest.ResponseRecorder) { +func runMw(t *testing.T, ctx *contextmodel.ReqContext, httpmethod string, path string, webparams map[string]string, mw func(c *contextmodel.ReqContext)) (*contextmodel.ReqContext, *httptest.ResponseRecorder) { // create valid request context and set 0 values if they don't exist if ctx == nil { - ctx = &models.ReqContext{} + ctx = &contextmodel.ReqContext{} } if ctx.Context == nil { ctx.Context = &web.Context{} diff --git a/pkg/services/publicdashboards/api/query.go b/pkg/services/publicdashboards/api/query.go index 2f9473ca9f0..4f4583d621f 100644 --- a/pkg/services/publicdashboards/api/query.go +++ b/pkg/services/publicdashboards/api/query.go @@ -6,18 +6,18 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" - "github.com/grafana/grafana/pkg/services/publicdashboards/internal/tokens" . "github.com/grafana/grafana/pkg/services/publicdashboards/models" + "github.com/grafana/grafana/pkg/services/publicdashboards/validation" "github.com/grafana/grafana/pkg/web" ) // ViewPublicDashboard Gets public dashboard // GET /api/public/dashboards/:accessToken -func (api *Api) ViewPublicDashboard(c *models.ReqContext) response.Response { +func (api *Api) ViewPublicDashboard(c *contextmodel.ReqContext) response.Response { accessToken := web.Params(c.Req)[":accessToken"] - if !tokens.IsValidAccessToken(accessToken) { + if !validation.IsValidAccessToken(accessToken) { return response.Err(ErrInvalidAccessToken.Errorf("ViewPublicDashboard: invalid access token")) } @@ -53,9 +53,9 @@ func (api *Api) ViewPublicDashboard(c *models.ReqContext) response.Response { // QueryPublicDashboard returns all results for a given panel on a public dashboard // POST /api/public/dashboard/:accessToken/panels/:panelId/query -func (api *Api) QueryPublicDashboard(c *models.ReqContext) response.Response { +func (api *Api) QueryPublicDashboard(c *contextmodel.ReqContext) response.Response { accessToken := web.Params(c.Req)[":accessToken"] - if !tokens.IsValidAccessToken(accessToken) { + if !validation.IsValidAccessToken(accessToken) { return response.Err(ErrInvalidAccessToken.Errorf("QueryPublicDashboard: invalid access token")) } @@ -79,9 +79,9 @@ func (api *Api) QueryPublicDashboard(c *models.ReqContext) response.Response { // GetAnnotations returns annotations for a public dashboard // GET /api/public/dashboards/:accessToken/annotations -func (api *Api) GetAnnotations(c *models.ReqContext) response.Response { +func (api *Api) GetAnnotations(c *contextmodel.ReqContext) response.Response { accessToken := web.Params(c.Req)[":accessToken"] - if !tokens.IsValidAccessToken(accessToken) { + if !validation.IsValidAccessToken(accessToken) { return response.Err(ErrInvalidAccessToken.Errorf("GetAnnotations: invalid access token")) } diff --git a/pkg/services/publicdashboards/database/database_test.go b/pkg/services/publicdashboards/database/database_test.go index ba4a6eab5ad..d8da2b866f7 100644 --- a/pkg/services/publicdashboards/database/database_test.go +++ b/pkg/services/publicdashboards/database/database_test.go @@ -10,8 +10,8 @@ import ( "github.com/grafana/grafana/pkg/services/dashboards" dashboardsDB "github.com/grafana/grafana/pkg/services/dashboards/database" "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/services/publicdashboards/internal/tokens" . "github.com/grafana/grafana/pkg/services/publicdashboards/models" + "github.com/grafana/grafana/pkg/services/publicdashboards/service" "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/tag/tagimpl" "github.com/grafana/grafana/pkg/setting" @@ -693,7 +693,7 @@ func insertPublicDashboard(t *testing.T, publicdashboardStore *PublicDashboardSt uid := util.GenerateShortUID() - accessToken, err := tokens.GenerateAccessToken() + accessToken, err := service.GenerateAccessToken() require.NoError(t, err) cmd := SavePublicDashboardCommand{ diff --git a/pkg/services/publicdashboards/internal/tokens/tokens.go b/pkg/services/publicdashboards/internal/tokens/tokens.go deleted file mode 100644 index 4d4f3accf0d..00000000000 --- a/pkg/services/publicdashboards/internal/tokens/tokens.go +++ /dev/null @@ -1,29 +0,0 @@ -package tokens - -import ( - "fmt" - - "github.com/google/uuid" - "github.com/grafana/grafana/pkg/util" -) - -// GenerateAccessToken generates an uuid formatted without dashes to use as access token -func GenerateAccessToken() (string, error) { - token, err := uuid.NewRandom() - if err != nil { - return "", err - } - return fmt.Sprintf("%x", token[:]), nil -} - -// IsValidAccessToken asserts that an accessToken is a valid uuid -func IsValidAccessToken(token string) bool { - _, err := uuid.Parse(token) - return err == nil -} - -// IsValidShortUID checks that the uid is not blank and contains valid -// characters. Wraps utils.IsValidShortUID -func IsValidShortUID(uid string) bool { - return uid != "" && util.IsValidShortUID(uid) -} diff --git a/pkg/services/publicdashboards/internal/tokens/tokens_test.go b/pkg/services/publicdashboards/internal/tokens/tokens_test.go deleted file mode 100644 index b04ba221f10..00000000000 --- a/pkg/services/publicdashboards/internal/tokens/tokens_test.go +++ /dev/null @@ -1,54 +0,0 @@ -package tokens - -import ( - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestGenerateAccessToken(t *testing.T) { - accessToken, err := GenerateAccessToken() - - t.Run("length", func(t *testing.T) { - require.NoError(t, err) - assert.Equal(t, 32, len(accessToken)) - }) - - t.Run("no - ", func(t *testing.T) { - assert.False(t, strings.Contains("-", accessToken)) - }) -} - -func TestValidAccessToken(t *testing.T) { - t.Run("true", func(t *testing.T) { - uuid, _ := GenerateAccessToken() - assert.True(t, IsValidAccessToken(uuid)) - }) - - t.Run("false when blank", func(t *testing.T) { - assert.False(t, IsValidAccessToken("")) - }) - - t.Run("false when can't be parsed by uuid lib", func(t *testing.T) { - // too long - assert.False(t, IsValidAccessToken("0123456789012345678901234567890123456789")) - }) -} - -// we just check base cases since this wraps utils.IsValidShortUID which has -// test coverage -func TestValidUid(t *testing.T) { - t.Run("true", func(t *testing.T) { - assert.True(t, IsValidShortUID("afqrz7jZZ")) - }) - - t.Run("false when blank", func(t *testing.T) { - assert.False(t, IsValidShortUID("")) - }) - - t.Run("false when invalid chars", func(t *testing.T) { - assert.False(t, IsValidShortUID("afqrz7j%%")) - }) -} diff --git a/pkg/services/publicdashboards/public_dashboard_service_mock.go b/pkg/services/publicdashboards/public_dashboard_service_mock.go index 883c51e1b80..3fcf2bf2f87 100644 --- a/pkg/services/publicdashboards/public_dashboard_service_mock.go +++ b/pkg/services/publicdashboards/public_dashboard_service_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.16.0. DO NOT EDIT. +// Code generated by mockery v2.14.0. DO NOT EDIT. package publicdashboards @@ -102,6 +102,29 @@ func (_m *FakePublicDashboardService) ExistsEnabledByDashboardUid(ctx context.Co return r0, r1 } +// Find provides a mock function with given fields: ctx, uid +func (_m *FakePublicDashboardService) Find(ctx context.Context, uid string) (*models.PublicDashboard, error) { + ret := _m.Called(ctx, uid) + + var r0 *models.PublicDashboard + if rf, ok := ret.Get(0).(func(context.Context, string) *models.PublicDashboard); ok { + r0 = rf(ctx, uid) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.PublicDashboard) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, uid) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // FindAll provides a mock function with given fields: ctx, u, orgId func (_m *FakePublicDashboardService) FindAll(ctx context.Context, u *user.SignedInUser, orgId int64) ([]models.PublicDashboardListResponse, error) { ret := _m.Called(ctx, u, orgId) diff --git a/pkg/services/publicdashboards/publicdashboard.go b/pkg/services/publicdashboards/publicdashboard.go index b301e9be8ad..017c37f4152 100644 --- a/pkg/services/publicdashboards/publicdashboard.go +++ b/pkg/services/publicdashboards/publicdashboard.go @@ -21,6 +21,7 @@ type Service interface { FindAnnotations(ctx context.Context, reqDTO AnnotationsQueryDTO, accessToken string) ([]AnnotationEvent, error) FindDashboard(ctx context.Context, orgId int64, dashboardUid string) (*dashboards.Dashboard, error) FindAll(ctx context.Context, u *user.SignedInUser, orgId int64) ([]PublicDashboardListResponse, error) + Find(ctx context.Context, uid string) (*PublicDashboard, error) Create(ctx context.Context, u *user.SignedInUser, dto *SavePublicDashboardDTO) (*PublicDashboard, error) Update(ctx context.Context, u *user.SignedInUser, dto *SavePublicDashboardDTO) (*PublicDashboard, error) Delete(ctx context.Context, orgId int64, uid string) error diff --git a/pkg/services/publicdashboards/service/service.go b/pkg/services/publicdashboards/service/service.go index 812257d6850..2d557248e4e 100644 --- a/pkg/services/publicdashboards/service/service.go +++ b/pkg/services/publicdashboards/service/service.go @@ -3,15 +3,16 @@ package service import ( "context" "errors" + "fmt" "time" + "github.com/google/uuid" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/publicdashboards" - "github.com/grafana/grafana/pkg/services/publicdashboards/internal/tokens" . "github.com/grafana/grafana/pkg/services/publicdashboards/models" "github.com/grafana/grafana/pkg/services/publicdashboards/validation" "github.com/grafana/grafana/pkg/services/query" @@ -60,6 +61,14 @@ func ProvideService( } } +func (pd *PublicDashboardServiceImpl) Find(ctx context.Context, uid string) (*PublicDashboard, error) { + pubdash, err := pd.store.Find(ctx, uid) + if err != nil { + return nil, ErrInternalServerError.Errorf("Find: failed to find public dashboard%w", err) + } + return pubdash, nil +} + // FindDashboard Gets a dashboard by Uid func (pd *PublicDashboardServiceImpl) FindDashboard(ctx context.Context, orgId int64, dashboardUid string) (*dashboards.Dashboard, error) { dash, err := pd.store.FindDashboard(ctx, orgId, dashboardUid) @@ -281,7 +290,7 @@ func (pd *PublicDashboardServiceImpl) NewPublicDashboardAccessToken(ctx context. var accessToken string for i := 0; i < 3; i++ { var err error - accessToken, err = tokens.GenerateAccessToken() + accessToken, err = GenerateAccessToken() if err != nil { continue } @@ -396,3 +405,12 @@ func publicDashboardIsEnabledChanged(existingPubdash *PublicDashboard, newPubdas isEnabledChanged := existingPubdash != nil && newPubdash.IsEnabled != existingPubdash.IsEnabled return newDashCreated || isEnabledChanged } + +// GenerateAccessToken generates an uuid formatted without dashes to use as access token +func GenerateAccessToken() (string, error) { + token, err := uuid.NewRandom() + if err != nil { + return "", err + } + return fmt.Sprintf("%x", token[:]), nil +} diff --git a/pkg/services/publicdashboards/service/service_test.go b/pkg/services/publicdashboards/service/service_test.go index ebc16b51d80..8a1acd1389b 100644 --- a/pkg/services/publicdashboards/service/service_test.go +++ b/pkg/services/publicdashboards/service/service_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "strings" "testing" "time" @@ -18,8 +19,8 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" . "github.com/grafana/grafana/pkg/services/publicdashboards" "github.com/grafana/grafana/pkg/services/publicdashboards/database" - "github.com/grafana/grafana/pkg/services/publicdashboards/internal/tokens" . "github.com/grafana/grafana/pkg/services/publicdashboards/models" + "github.com/grafana/grafana/pkg/services/publicdashboards/validation" "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/serviceaccounts/tests" "github.com/grafana/grafana/pkg/services/tag/tagimpl" @@ -914,7 +915,7 @@ func TestPublicDashboardServiceImpl_NewPublicDashboardAccessToken(t *testing.T) if err == nil { assert.NotEqual(t, got, tt.want, "NewPublicDashboardAccessToken(%v)", tt.args.ctx) - assert.True(t, tokens.IsValidAccessToken(got), "NewPublicDashboardAccessToken(%v)", tt.args.ctx) + assert.True(t, validation.IsValidAccessToken(got), "NewPublicDashboardAccessToken(%v)", tt.args.ctx) store.AssertNumberOfCalls(t, "FindByAccessToken", 1) } else { store.AssertNumberOfCalls(t, "FindByAccessToken", 3) @@ -1028,3 +1029,16 @@ func insertTestDashboard(t *testing.T, dashboardStore *dashboardsDB.DashboardSto dash.Data.Set("uid", dash.UID) return dash } + +func TestGenerateAccessToken(t *testing.T) { + accessToken, err := GenerateAccessToken() + + t.Run("length", func(t *testing.T) { + require.NoError(t, err) + assert.Equal(t, 32, len(accessToken)) + }) + + t.Run("no - ", func(t *testing.T) { + assert.False(t, strings.Contains("-", accessToken)) + }) +} diff --git a/pkg/services/publicdashboards/validation/validation.go b/pkg/services/publicdashboards/validation/validation.go index 6f2c57c83dd..2094cf3545d 100644 --- a/pkg/services/publicdashboards/validation/validation.go +++ b/pkg/services/publicdashboards/validation/validation.go @@ -1,9 +1,11 @@ package validation import ( + "github.com/google/uuid" "github.com/grafana/grafana/pkg/services/dashboards" . "github.com/grafana/grafana/pkg/services/publicdashboards/models" "github.com/grafana/grafana/pkg/tsdb/legacydata" + "github.com/grafana/grafana/pkg/util" ) func ValidatePublicDashboard(dto *SavePublicDashboardDTO, dashboard *dashboards.Dashboard) error { @@ -44,3 +46,15 @@ func ValidateQueryPublicDashboardRequest(req PublicDashboardQueryDTO, pd *Public return nil } + +// IsValidAccessToken asserts that an accessToken is a valid uuid +func IsValidAccessToken(token string) bool { + _, err := uuid.Parse(token) + return err == nil +} + +// IsValidShortUID checks that the uid is not blank and contains valid +// characters. Wraps utils.IsValidShortUID +func IsValidShortUID(uid string) bool { + return uid != "" && util.IsValidShortUID(uid) +} diff --git a/pkg/services/publicdashboards/validation/validation_test.go b/pkg/services/publicdashboards/validation/validation_test.go index 4ee281e20fb..5ba9dcd8c30 100644 --- a/pkg/services/publicdashboards/validation/validation_test.go +++ b/pkg/services/publicdashboards/validation/validation_test.go @@ -6,6 +6,7 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/services/dashboards" . "github.com/grafana/grafana/pkg/services/publicdashboards/models" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -157,3 +158,35 @@ func TestValidateQueryPublicDashboardRequest(t *testing.T) { }) } } + +func TestValidAccessToken(t *testing.T) { + t.Run("true", func(t *testing.T) { + uuid := "da82510c2aa64d78a2e87fef36c58e89" + assert.True(t, IsValidAccessToken(uuid)) + }) + + t.Run("false when blank", func(t *testing.T) { + assert.False(t, IsValidAccessToken("")) + }) + + t.Run("false when can't be parsed by uuid lib", func(t *testing.T) { + // too long + assert.False(t, IsValidAccessToken("0123456789012345678901234567890123456789")) + }) +} + +// we just check base cases since this wraps utils.IsValidShortUID which has +// test coverage +func TestValidUid(t *testing.T) { + t.Run("true", func(t *testing.T) { + assert.True(t, IsValidShortUID("afqrz7jZZ")) + }) + + t.Run("false when blank", func(t *testing.T) { + assert.False(t, IsValidShortUID("")) + }) + + t.Run("false when invalid chars", func(t *testing.T) { + assert.False(t, IsValidShortUID("afqrz7j%%")) + }) +} diff --git a/pkg/services/query/models.go b/pkg/services/query/models.go index 7e209923575..bb8bf8db6f0 100644 --- a/pkg/services/query/models.go +++ b/pkg/services/query/models.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/services/contexthandler" "github.com/grafana/grafana/pkg/services/datasources" diff --git a/pkg/services/query/query_test.go b/pkg/services/query/query_test.go index e9c9c4a5c57..188fea5516a 100644 --- a/pkg/services/query/query_test.go +++ b/pkg/services/query/query_test.go @@ -16,11 +16,11 @@ import ( "github.com/grafana/grafana/pkg/expr" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models/roletype" "github.com/grafana/grafana/pkg/plugins" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" fakeDatasources "github.com/grafana/grafana/pkg/services/datasources/fakes" dsSvc "github.com/grafana/grafana/pkg/services/datasources/service" @@ -211,7 +211,7 @@ func TestParseMetricRequest(t *testing.T) { httpreq, err := http.NewRequest(http.MethodPost, "http://localhost/", bytes.NewReader([]byte{})) require.NoError(t, err) - reqCtx := &models.ReqContext{ + reqCtx := &contextmodel.ReqContext{ Context: &web.Context{}, } ctx := ctxkey.Set(context.Background(), reqCtx) @@ -325,7 +325,7 @@ func TestQueryDataMultipleSources(t *testing.T) { httpreq, err := http.NewRequest(http.MethodPost, "http://localhost/ds/query?expression=true", bytes.NewReader([]byte{})) require.NoError(t, err) - reqCtx := &models.ReqContext{ + reqCtx := &contextmodel.ReqContext{ Context: &web.Context{}, } ctx := ctxkey.Set(context.Background(), reqCtx) diff --git a/pkg/services/queryhistory/api.go b/pkg/services/queryhistory/api.go index 8b99fadb467..43a9fed05eb 100644 --- a/pkg/services/queryhistory/api.go +++ b/pkg/services/queryhistory/api.go @@ -6,7 +6,7 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/tsdb/legacydata" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/web" @@ -36,7 +36,7 @@ func (s *QueryHistoryService) registerAPIEndpoints() { // 400: badRequestError // 401: unauthorisedError // 500: internalServerError -func (s *QueryHistoryService) createHandler(c *models.ReqContext) response.Response { +func (s *QueryHistoryService) createHandler(c *contextmodel.ReqContext) response.Response { cmd := CreateQueryInQueryHistoryCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -62,7 +62,7 @@ func (s *QueryHistoryService) createHandler(c *models.ReqContext) response.Respo // 200: getQueryHistorySearchResponse // 401: unauthorisedError // 500: internalServerError -func (s *QueryHistoryService) searchHandler(c *models.ReqContext) response.Response { +func (s *QueryHistoryService) searchHandler(c *contextmodel.ReqContext) response.Response { timeRange := legacydata.NewDataTimeRange(c.Query("from"), c.Query("to")) query := SearchInQueryHistoryQuery{ @@ -94,7 +94,7 @@ func (s *QueryHistoryService) searchHandler(c *models.ReqContext) response.Respo // 200: getQueryHistoryDeleteQueryResponse // 401: unauthorisedError // 500: internalServerError -func (s *QueryHistoryService) deleteHandler(c *models.ReqContext) response.Response { +func (s *QueryHistoryService) deleteHandler(c *contextmodel.ReqContext) response.Response { queryUID := web.Params(c.Req)[":uid"] if len(queryUID) > 0 && !util.IsValidShortUID(queryUID) { return response.Error(http.StatusNotFound, "Query in query history not found", nil) @@ -122,7 +122,7 @@ func (s *QueryHistoryService) deleteHandler(c *models.ReqContext) response.Respo // 400: badRequestError // 401: unauthorisedError // 500: internalServerError -func (s *QueryHistoryService) patchCommentHandler(c *models.ReqContext) response.Response { +func (s *QueryHistoryService) patchCommentHandler(c *contextmodel.ReqContext) response.Response { queryUID := web.Params(c.Req)[":uid"] if len(queryUID) > 0 && !util.IsValidShortUID(queryUID) { return response.Error(http.StatusNotFound, "Query in query history not found", nil) @@ -151,7 +151,7 @@ func (s *QueryHistoryService) patchCommentHandler(c *models.ReqContext) response // 200: getQueryHistoryResponse // 401: unauthorisedError // 500: internalServerError -func (s *QueryHistoryService) starHandler(c *models.ReqContext) response.Response { +func (s *QueryHistoryService) starHandler(c *contextmodel.ReqContext) response.Response { queryUID := web.Params(c.Req)[":uid"] if len(queryUID) > 0 && !util.IsValidShortUID(queryUID) { return response.Error(http.StatusNotFound, "Query in query history not found", nil) @@ -175,7 +175,7 @@ func (s *QueryHistoryService) starHandler(c *models.ReqContext) response.Respons // 200: getQueryHistoryResponse // 401: unauthorisedError // 500: internalServerError -func (s *QueryHistoryService) unstarHandler(c *models.ReqContext) response.Response { +func (s *QueryHistoryService) unstarHandler(c *contextmodel.ReqContext) response.Response { queryUID := web.Params(c.Req)[":uid"] if len(queryUID) > 0 && !util.IsValidShortUID(queryUID) { return response.Error(http.StatusNotFound, "Query in query history not found", nil) @@ -200,7 +200,7 @@ func (s *QueryHistoryService) unstarHandler(c *models.ReqContext) response.Respo // 400: badRequestError // 401: unauthorisedError // 500: internalServerError -func (s *QueryHistoryService) migrateHandler(c *models.ReqContext) response.Response { +func (s *QueryHistoryService) migrateHandler(c *contextmodel.ReqContext) response.Response { cmd := MigrateQueriesToQueryHistoryCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) diff --git a/pkg/services/queryhistory/queryhistory_create_test.go b/pkg/services/queryhistory/queryhistory_create_test.go index a04d967a64f..3e14f106890 100644 --- a/pkg/services/queryhistory/queryhistory_create_test.go +++ b/pkg/services/queryhistory/queryhistory_create_test.go @@ -3,8 +3,9 @@ package queryhistory import ( "testing" - "github.com/grafana/grafana/pkg/components/simplejson" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/components/simplejson" ) func TestIntegrationCreateQueryInQueryHistory(t *testing.T) { diff --git a/pkg/services/queryhistory/queryhistory_delete_stale_test.go b/pkg/services/queryhistory/queryhistory_delete_stale_test.go index 4d1a5e1a271..84ed7839572 100644 --- a/pkg/services/queryhistory/queryhistory_delete_stale_test.go +++ b/pkg/services/queryhistory/queryhistory_delete_stale_test.go @@ -5,8 +5,9 @@ import ( "testing" "time" - "github.com/grafana/grafana/pkg/web" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/web" ) func TestIntegrationDeleteStaleQueryFromQueryHistory(t *testing.T) { diff --git a/pkg/services/queryhistory/queryhistory_migrate_test.go b/pkg/services/queryhistory/queryhistory_migrate_test.go index dc4156dbc37..a8f5667eb25 100644 --- a/pkg/services/queryhistory/queryhistory_migrate_test.go +++ b/pkg/services/queryhistory/queryhistory_migrate_test.go @@ -5,8 +5,9 @@ import ( "testing" "time" - "github.com/grafana/grafana/pkg/components/simplejson" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/components/simplejson" ) func TestIntegrationMigrateQueriesToQueryHistory(t *testing.T) { diff --git a/pkg/services/queryhistory/queryhistory_patch_test.go b/pkg/services/queryhistory/queryhistory_patch_test.go index 14e0dabe37f..b9280367b1e 100644 --- a/pkg/services/queryhistory/queryhistory_patch_test.go +++ b/pkg/services/queryhistory/queryhistory_patch_test.go @@ -3,8 +3,9 @@ package queryhistory import ( "testing" - "github.com/grafana/grafana/pkg/web" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/web" ) func TestIntegrationPatchQueryCommentInQueryHistory(t *testing.T) { diff --git a/pkg/services/queryhistory/queryhistory_star_test.go b/pkg/services/queryhistory/queryhistory_star_test.go index a5fab3430c9..408435ef065 100644 --- a/pkg/services/queryhistory/queryhistory_star_test.go +++ b/pkg/services/queryhistory/queryhistory_star_test.go @@ -3,8 +3,9 @@ package queryhistory import ( "testing" - "github.com/grafana/grafana/pkg/web" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/web" ) func TestIntegrationStarQueryInQueryHistory(t *testing.T) { diff --git a/pkg/services/queryhistory/queryhistory_test.go b/pkg/services/queryhistory/queryhistory_test.go index 2976281b6d8..e598c45b67f 100644 --- a/pkg/services/queryhistory/queryhistory_test.go +++ b/pkg/services/queryhistory/queryhistory_test.go @@ -15,7 +15,7 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgimpl" "github.com/grafana/grafana/pkg/services/quota/quotatest" @@ -35,7 +35,7 @@ var ( type scenarioContext struct { ctx *web.Context service *QueryHistoryService - reqContext *models.ReqContext + reqContext *contextmodel.ReqContext sqlStore db.DB initialResult QueryHistoryResponse } @@ -82,7 +82,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo ctx: &ctx, service: &service, sqlStore: sqlStore, - reqContext: &models.ReqContext{ + reqContext: &contextmodel.ReqContext{ Context: &ctx, SignedInUser: &usr, }, diff --git a/pkg/services/queryhistory/queryhistory_unstar_test.go b/pkg/services/queryhistory/queryhistory_unstar_test.go index eff47541fdc..0953a8f01d6 100644 --- a/pkg/services/queryhistory/queryhistory_unstar_test.go +++ b/pkg/services/queryhistory/queryhistory_unstar_test.go @@ -3,8 +3,9 @@ package queryhistory import ( "testing" - "github.com/grafana/grafana/pkg/web" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/web" ) func TestIntegrationUnstarQueryInQueryHistory(t *testing.T) { diff --git a/pkg/services/querylibrary/querylibraryimpl/http.go b/pkg/services/querylibrary/querylibraryimpl/http.go index 34c25f3054c..10ee1d1fc91 100644 --- a/pkg/services/querylibrary/querylibraryimpl/http.go +++ b/pkg/services/querylibrary/querylibraryimpl/http.go @@ -9,7 +9,7 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/querylibrary" ) @@ -21,7 +21,7 @@ func (s *queriesServiceHTTPHandler) IsDisabled() bool { return s.service.IsDisabled() } -func (s *queriesServiceHTTPHandler) delete(c *models.ReqContext) response.Response { +func (s *queriesServiceHTTPHandler) delete(c *contextmodel.ReqContext) response.Response { uid := c.Query("uid") err := s.service.Delete(c.Req.Context(), c.SignedInUser, uid) if err != nil { @@ -40,7 +40,7 @@ func (s *queriesServiceHTTPHandler) RegisterHTTPRoutes(routes routing.RouteRegis routes.Delete("/", reqSignedIn, routing.Wrap(s.delete)) } -func (s *queriesServiceHTTPHandler) getBatch(c *models.ReqContext) response.Response { +func (s *queriesServiceHTTPHandler) getBatch(c *contextmodel.ReqContext) response.Response { uids := c.QueryStrings("uid") queries, err := s.service.GetBatch(c.Req.Context(), c.SignedInUser, uids) @@ -51,7 +51,7 @@ func (s *queriesServiceHTTPHandler) getBatch(c *models.ReqContext) response.Resp return response.JSON(200, queries) } -func (s *queriesServiceHTTPHandler) update(c *models.ReqContext) response.Response { +func (s *queriesServiceHTTPHandler) update(c *contextmodel.ReqContext) response.Response { body, err := io.ReadAll(c.Req.Body) if err != nil { return response.Error(500, "error reading bytes", err) diff --git a/pkg/services/querylibrary/tests/common.go b/pkg/services/querylibrary/tests/common.go index f8cfe67ccfa..fddced65a0b 100644 --- a/pkg/services/querylibrary/tests/common.go +++ b/pkg/services/querylibrary/tests/common.go @@ -4,6 +4,8 @@ import ( "fmt" "testing" + "github.com/stretchr/testify/require" + apikeygenprefix "github.com/grafana/grafana/pkg/components/apikeygenprefixed" "github.com/grafana/grafana/pkg/server" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -12,7 +14,6 @@ import ( saTests "github.com/grafana/grafana/pkg/services/serviceaccounts/tests" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/tests/testinfra" - "github.com/stretchr/testify/require" ) func createServiceAccountAdminToken(t *testing.T, name string, env *server.TestEnv) (string, *user.SignedInUser) { diff --git a/pkg/services/quota/quota.go b/pkg/services/quota/quota.go index 13045f41de2..700db61a403 100644 --- a/pkg/services/quota/quota.go +++ b/pkg/services/quota/quota.go @@ -3,7 +3,7 @@ package quota import ( "context" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" ) type Service interface { @@ -16,7 +16,7 @@ type Service interface { // If the cmd.UseID is set, then the user quota are updated. Update(ctx context.Context, cmd *UpdateQuotaCmd) error // QuotaReached is called by the quota middleware for applying quota enforcement to API handlers - QuotaReached(c *models.ReqContext, targetSrv TargetSrv) (bool, error) + QuotaReached(c *contextmodel.ReqContext, targetSrv TargetSrv) (bool, error) // CheckQuotaReached checks if the quota limitations have been reached for a specific service CheckQuotaReached(ctx context.Context, targetSrv TargetSrv, scopeParams *ScopeParameters) (bool, error) // DeleteQuotaForUser deletes custom quota limitations for the user diff --git a/pkg/services/quota/quotaimpl/quota.go b/pkg/services/quota/quotaimpl/quota.go index 65763f20463..b4213fd10a5 100644 --- a/pkg/services/quota/quotaimpl/quota.go +++ b/pkg/services/quota/quotaimpl/quota.go @@ -4,18 +4,19 @@ import ( "context" "sync" + "golang.org/x/sync/errgroup" + "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/setting" - "golang.org/x/sync/errgroup" ) type serviceDisabled struct { } -func (s *serviceDisabled) QuotaReached(c *models.ReqContext, targetSrv quota.TargetSrv) (bool, error) { +func (s *serviceDisabled) QuotaReached(c *contextmodel.ReqContext, targetSrv quota.TargetSrv) (bool, error) { return false, nil } @@ -75,7 +76,7 @@ func (s *service) IsDisabled() bool { } // QuotaReached checks that quota is reached for a target. Runs CheckQuotaReached and take context and scope parameters from the request context -func (s *service) QuotaReached(c *models.ReqContext, targetSrv quota.TargetSrv) (bool, error) { +func (s *service) QuotaReached(c *contextmodel.ReqContext, targetSrv quota.TargetSrv) (bool, error) { // No request context means this is a background service, like LDAP Background Sync if c == nil { return false, nil diff --git a/pkg/services/quota/quotatest/fake.go b/pkg/services/quota/quotatest/fake.go index d62267d9276..16fa2bc6403 100644 --- a/pkg/services/quota/quotatest/fake.go +++ b/pkg/services/quota/quotatest/fake.go @@ -3,7 +3,7 @@ package quotatest import ( "context" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/quota" ) @@ -24,7 +24,7 @@ func (f *FakeQuotaService) Update(ctx context.Context, cmd *quota.UpdateQuotaCmd return nil } -func (f *FakeQuotaService) QuotaReached(c *models.ReqContext, target quota.TargetSrv) (bool, error) { +func (f *FakeQuotaService) QuotaReached(c *contextmodel.ReqContext, target quota.TargetSrv) (bool, error) { return f.reached, f.err } diff --git a/pkg/services/rendering/mock.go b/pkg/services/rendering/mock.go index 6d5d0f33f92..6098d59e204 100644 --- a/pkg/services/rendering/mock.go +++ b/pkg/services/rendering/mock.go @@ -9,6 +9,7 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" + models "github.com/grafana/grafana/pkg/models" ) diff --git a/pkg/services/rendering/rendering_test.go b/pkg/services/rendering/rendering_test.go index 6bb798638f1..0cb8edd2a8e 100644 --- a/pkg/services/rendering/rendering_test.go +++ b/pkg/services/rendering/rendering_test.go @@ -9,12 +9,13 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestGetUrl(t *testing.T) { diff --git a/pkg/services/search/model/model.go b/pkg/services/search/model/model.go new file mode 100644 index 00000000000..ac6828fe415 --- /dev/null +++ b/pkg/services/search/model/model.go @@ -0,0 +1,62 @@ +package model + +import ( + "strings" + + "github.com/grafana/grafana/pkg/services/sqlstore/searchstore" +) + +type SortOption struct { + Name string + DisplayName string + Description string + Index int + MetaName string + Filter []SortOptionFilter +} + +type SortOptionFilter interface { + searchstore.FilterOrderBy +} + +type HitType string + +const ( + DashHitDB HitType = "dash-db" + DashHitHome HitType = "dash-home" + DashHitFolder HitType = "dash-folder" +) + +type Hit struct { + ID int64 `json:"id"` + UID string `json:"uid"` + Title string `json:"title"` + URI string `json:"uri"` + URL string `json:"url"` + Slug string `json:"slug"` + Type HitType `json:"type"` + Tags []string `json:"tags"` + IsStarred bool `json:"isStarred"` + FolderID int64 `json:"folderId,omitempty"` + FolderUID string `json:"folderUid,omitempty"` + FolderTitle string `json:"folderTitle,omitempty"` + FolderURL string `json:"folderUrl,omitempty"` + SortMeta int64 `json:"sortMeta"` + SortMetaName string `json:"sortMetaName,omitempty"` +} + +type HitList []*Hit + +func (s HitList) Len() int { return len(s) } +func (s HitList) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s HitList) Less(i, j int) bool { + if s[i].Type == "dash-folder" && s[j].Type == "dash-db" { + return true + } + + if s[i].Type == "dash-db" && s[j].Type == "dash-folder" { + return false + } + + return strings.ToLower(s[i].Title) < strings.ToLower(s[j].Title) +} diff --git a/pkg/services/search/service.go b/pkg/services/search/service.go index 8f45effe504..64d130023e2 100644 --- a/pkg/services/search/service.go +++ b/pkg/services/search/service.go @@ -6,17 +6,16 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/search/model" "github.com/grafana/grafana/pkg/services/star" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" - - "github.com/grafana/grafana/pkg/models" ) func ProvideService(cfg *setting.Cfg, sqlstore db.DB, starService star.Service, dashboardService dashboards.DashboardService) *SearchService { s := &SearchService{ Cfg: cfg, - sortOptions: map[string]models.SortOption{ + sortOptions: map[string]model.SortOption{ SortAlphaAsc.Name: SortAlphaAsc, SortAlphaDesc.Name: SortAlphaDesc, }, @@ -42,17 +41,17 @@ type Query struct { Permission dashboards.PermissionType Sort string - Result models.HitList + Result model.HitList } type Service interface { SearchHandler(context.Context, *Query) error - SortOptions() []models.SortOption + SortOptions() []model.SortOption } type SearchService struct { Cfg *setting.Cfg - sortOptions map[string]models.SortOption + sortOptions map[string]model.SortOption sqlstore db.DB starService star.Service dashboardService dashboards.DashboardService @@ -95,8 +94,8 @@ func (s *SearchService) SearchHandler(ctx context.Context, query *Query) error { return nil } -func sortedHits(unsorted models.HitList) models.HitList { - hits := make(models.HitList, 0) +func sortedHits(unsorted model.HitList) model.HitList { + hits := make(model.HitList, 0) hits = append(hits, unsorted...) sort.Sort(hits) @@ -108,7 +107,7 @@ func sortedHits(unsorted models.HitList) models.HitList { return hits } -func (s *SearchService) setStarredDashboards(ctx context.Context, userID int64, hits []*models.Hit) error { +func (s *SearchService) setStarredDashboards(ctx context.Context, userID int64, hits []*model.Hit) error { query := star.GetUserStarsQuery{ UserID: userID, } diff --git a/pkg/services/search/service_test.go b/pkg/services/search/service_test.go index 19b94758b68..c0c9245b01d 100644 --- a/pkg/services/search/service_test.go +++ b/pkg/services/search/service_test.go @@ -9,8 +9,8 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/db/dbtest" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/search/model" "github.com/grafana/grafana/pkg/services/star" "github.com/grafana/grafana/pkg/services/star/startest" "github.com/grafana/grafana/pkg/services/user" @@ -24,12 +24,12 @@ func TestSearch_SortedResults(t *testing.T) { ds := dashboards.NewFakeDashboardService(t) ds.On("SearchDashboards", mock.Anything, mock.AnythingOfType("*dashboards.FindPersistedDashboardsQuery")).Run(func(args mock.Arguments) { q := args.Get(1).(*dashboards.FindPersistedDashboardsQuery) - q.Result = models.HitList{ - &models.Hit{ID: 16, Title: "CCAA", Type: "dash-db", Tags: []string{"BB", "AA"}}, - &models.Hit{ID: 10, Title: "AABB", Type: "dash-db", Tags: []string{"CC", "AA"}}, - &models.Hit{ID: 15, Title: "BBAA", Type: "dash-db", Tags: []string{"EE", "AA", "BB"}}, - &models.Hit{ID: 25, Title: "bbAAa", Type: "dash-db", Tags: []string{"EE", "AA", "BB"}}, - &models.Hit{ID: 17, Title: "FOLDER", Type: "dash-folder"}, + q.Result = model.HitList{ + &model.Hit{ID: 16, Title: "CCAA", Type: "dash-db", Tags: []string{"BB", "AA"}}, + &model.Hit{ID: 10, Title: "AABB", Type: "dash-db", Tags: []string{"CC", "AA"}}, + &model.Hit{ID: 15, Title: "BBAA", Type: "dash-db", Tags: []string{"EE", "AA", "BB"}}, + &model.Hit{ID: 25, Title: "bbAAa", Type: "dash-db", Tags: []string{"EE", "AA", "BB"}}, + &model.Hit{ID: 17, Title: "FOLDER", Type: "dash-folder"}, } }).Return(nil) us.ExpectedSignedInUser = &user.SignedInUser{IsGrafanaAdmin: true} diff --git a/pkg/services/search/sorting.go b/pkg/services/search/sorting.go index 25658d0b2f9..1749e23fa14 100644 --- a/pkg/services/search/sorting.go +++ b/pkg/services/search/sorting.go @@ -3,26 +3,26 @@ package search import ( "sort" - "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/search/model" "github.com/grafana/grafana/pkg/services/sqlstore/searchstore" ) var ( - SortAlphaAsc = models.SortOption{ + SortAlphaAsc = model.SortOption{ Name: "alpha-asc", DisplayName: "Alphabetically (A–Z)", Description: "Sort results in an alphabetically ascending order", Index: 0, - Filter: []models.SortOptionFilter{ + Filter: []model.SortOptionFilter{ searchstore.TitleSorter{}, }, } - SortAlphaDesc = models.SortOption{ + SortAlphaDesc = model.SortOption{ Name: "alpha-desc", DisplayName: "Alphabetically (Z–A)", Description: "Sort results in an alphabetically descending order", Index: 0, - Filter: []models.SortOptionFilter{ + Filter: []model.SortOptionFilter{ searchstore.TitleSorter{Descending: true}, }, } @@ -30,12 +30,12 @@ var ( // RegisterSortOption allows for hooking in more search options from // other services. -func (s *SearchService) RegisterSortOption(option models.SortOption) { +func (s *SearchService) RegisterSortOption(option model.SortOption) { s.sortOptions[option.Name] = option } -func (s *SearchService) SortOptions() []models.SortOption { - opts := make([]models.SortOption, 0, len(s.sortOptions)) +func (s *SearchService) SortOptions() []model.SortOption { + opts := make([]model.SortOption, 0, len(s.sortOptions)) for _, o := range s.sortOptions { opts = append(opts, o) } diff --git a/pkg/services/searchV2/allowed_actions.go b/pkg/services/searchV2/allowed_actions.go index 2e9dd13907e..0b5f1064bd7 100644 --- a/pkg/services/searchV2/allowed_actions.go +++ b/pkg/services/searchV2/allowed_actions.go @@ -9,6 +9,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/datasources" diff --git a/pkg/services/searchV2/auth.go b/pkg/services/searchV2/auth.go index 9fa87551ae2..60437f543cc 100644 --- a/pkg/services/searchV2/auth.go +++ b/pkg/services/searchV2/auth.go @@ -7,21 +7,20 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/sqlstore/permissions" - "github.com/grafana/grafana/pkg/services/sqlstore/searchstore" "github.com/grafana/grafana/pkg/services/user" ) // ResourceFilter checks if a given a uid (resource identifier) check if we have the requested permission -type ResourceFilter func(uid string) bool +type ResourceFilter func(kind entityKind, uid, parentUID string) bool // FutureAuthService eventually implemented by the security service type FutureAuthService interface { GetDashboardReadFilter(user *user.SignedInUser) (ResourceFilter, error) } -var _ FutureAuthService = (*simpleSQLAuthService)(nil) +var _ FutureAuthService = (*simpleAuthService)(nil) -type simpleSQLAuthService struct { +type simpleAuthService struct { sql db.DB ac accesscontrol.Service } @@ -30,22 +29,26 @@ type dashIdQueryResult struct { UID string `xorm:"uid"` } -func (a *simpleSQLAuthService) getDashboardTableAuthFilter(user *user.SignedInUser) searchstore.FilterWhere { - if a.ac.IsDisabled() { - return permissions.DashboardPermissionFilter{ - OrgRole: user.OrgRole, - OrgId: user.OrgID, - Dialect: a.sql.GetDialect(), - UserId: user.UserID, - PermissionLevel: dashboards.PERMISSION_VIEW, - } +func (a *simpleAuthService) GetDashboardReadFilter(user *user.SignedInUser) (ResourceFilter, error) { + if !a.ac.IsDisabled() { + canReadDashboard, canReadFolder := accesscontrol.Checker(user, dashboards.ActionDashboardsRead), accesscontrol.Checker(user, dashboards.ActionFoldersRead) + return func(kind entityKind, uid, parent string) bool { + if kind == entityKindFolder { + return canReadFolder(dashboards.ScopeFoldersProvider.GetResourceScopeUID(uid)) + } else if kind == entityKindDashboard { + return canReadDashboard(dashboards.ScopeDashboardsProvider.GetResourceScopeUID(uid), dashboards.ScopeFoldersProvider.GetResourceScopeUID(parent)) + } + return false + }, nil } - return permissions.NewAccessControlDashboardPermissionFilter(user, dashboards.PERMISSION_VIEW, "") -} - -func (a *simpleSQLAuthService) GetDashboardReadFilter(user *user.SignedInUser) (ResourceFilter, error) { - filter := a.getDashboardTableAuthFilter(user) + filter := permissions.DashboardPermissionFilter{ + OrgRole: user.OrgRole, + OrgId: user.OrgID, + Dialect: a.sql.GetDialect(), + UserId: user.UserID, + PermissionLevel: dashboards.PERMISSION_VIEW, + } rows := make([]*dashIdQueryResult, 0) err := a.sql.WithDbSession(context.Background(), func(sess *db.Session) error { @@ -72,7 +75,7 @@ func (a *simpleSQLAuthService) GetDashboardReadFilter(user *user.SignedInUser) ( uids[rows[i].UID] = true } - return func(uid string) bool { + return func(_ entityKind, uid, _ string) bool { return uids[uid] }, err } diff --git a/pkg/services/searchV2/bluge.go b/pkg/services/searchV2/bluge.go index 04bec20680c..223e46ec966 100644 --- a/pkg/services/searchV2/bluge.go +++ b/pkg/services/searchV2/bluge.go @@ -16,6 +16,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/slugify" + "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/store/entity" ) @@ -70,6 +71,7 @@ func initOrgIndex(dashboards []dashboard, logger log.Logger, extendDoc ExtendDas // First index the folders to construct folderIdLookup. folderIdLookup := make(map[int64]string, 50) + folderIdLookup[0] = folder.GeneralFolderUID for _, dash := range dashboards { if !dash.isFolder { continue @@ -83,9 +85,6 @@ func initOrgIndex(dashboards []dashboard, logger log.Logger, extendDoc ExtendDas return nil, err } uid := dash.uid - if uid == "" { - uid = "general" - } folderIdLookup[dash.id] = uid } diff --git a/pkg/services/searchV2/filter.go b/pkg/services/searchV2/filter.go index b64d9c2adae..b68c79297ba 100644 --- a/pkg/services/searchV2/filter.go +++ b/pkg/services/searchV2/filter.go @@ -2,6 +2,7 @@ package searchV2 import ( "regexp" + "strings" "github.com/blugelabs/bluge" "github.com/blugelabs/bluge/search" @@ -36,7 +37,7 @@ func (r entityKind) supportsAuthzCheck() bool { } var ( - permissionFilterFields = []string{documentFieldUID, documentFieldKind} + permissionFilterFields = []string{documentFieldUID, documentFieldKind, documentFieldLocation} panelIdFieldRegex = regexp.MustCompile(`^(.*)#([0-9]{1,4})$`) panelIdFieldDashboardUidSubmatchIndex = 1 panelIdFieldPanelIdSubmatchIndex = 2 @@ -65,7 +66,7 @@ func (q *PermissionFilter) logAccessDecision(decision bool, kind interface{}, id } } -func (q *PermissionFilter) canAccess(kind entityKind, id string) bool { +func (q *PermissionFilter) canAccess(kind entityKind, id, location string) bool { if !kind.supportsAuthzCheck() { q.logAccessDecision(false, kind, id, "entityDoesNotSupportAuthz") return false @@ -74,29 +75,28 @@ func (q *PermissionFilter) canAccess(kind entityKind, id string) bool { // TODO add `kind` to the `ResourceFilter` interface so that we can move the switch out of here // switch kind { - case entityKindFolder: - if id == "" { - q.logAccessDecision(true, kind, id, "generalFolder") - return true - } - fallthrough - case entityKindDashboard: - decision := q.filter(id) + case entityKindFolder, entityKindDashboard: + decision := q.filter(kind, id, location) q.logAccessDecision(decision, kind, id, "resourceFilter") return decision case entityKindPanel: matches := panelIdFieldRegex.FindStringSubmatch(id) - submatchCount := len(matches) if submatchCount != panelIdFieldRegexExpectedSubmatchCount { q.logAccessDecision(false, kind, id, "invalidPanelIdFieldRegexSubmatchCount", "submatchCount", submatchCount, "expectedSubmatchCount", panelIdFieldRegexExpectedSubmatchCount) return false } - dashboardUid := matches[panelIdFieldDashboardUidSubmatchIndex] - decision := q.filter(dashboardUid) - q.logAccessDecision(decision, kind, id, "resourceFilter", "dashboardUid", dashboardUid, "panelId", matches[panelIdFieldPanelIdSubmatchIndex]) + // Location is / + if !strings.HasSuffix(location, "/"+dashboardUid) { + q.logAccessDecision(false, kind, id, "invalidLocation", "location", location, "dashboardUid", dashboardUid) + return false + } + folderUid := location[:len(location)-len(dashboardUid)-1] + + decision := q.filter(entityKindDashboard, dashboardUid, folderUid) + q.logAccessDecision(decision, kind, id, "resourceFilter", "folderUid", folderUid, "dashboardUid", dashboardUid, "panelId", matches[panelIdFieldPanelIdSubmatchIndex]) return decision default: q.logAccessDecision(false, kind, id, "reason", "unknownKind") @@ -111,13 +111,18 @@ func (q *PermissionFilter) Searcher(i search.Reader, options search.SearcherOpti } s, err := searcher.NewMatchAllSearcher(i, 1, similarity.ConstantScorer(1), options) + if err != nil { + return nil, err + } return searcher.NewFilteringSearcher(s, func(d *search.DocumentMatch) bool { - var kind, id string + var kind, id, location string err := dvReader.VisitDocumentValues(d.Number, func(field string, term []byte) { if field == documentFieldKind { kind = string(term) } else if field == documentFieldUID { id = string(term) + } else if field == documentFieldLocation { + location = string(term) } }) if err != nil { @@ -131,6 +136,6 @@ func (q *PermissionFilter) Searcher(i search.Reader, options search.SearcherOpti return false } - return q.canAccess(e, id) + return q.canAccess(e, id, location) }), err } diff --git a/pkg/services/searchV2/http.go b/pkg/services/searchV2/http.go index 0d1da01ad83..09f99413643 100644 --- a/pkg/services/searchV2/http.go +++ b/pkg/services/searchV2/http.go @@ -7,11 +7,12 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/prometheus/client_golang/prometheus" + "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/models" - "github.com/prometheus/client_golang/prometheus" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" ) type SearchHTTPService interface { @@ -30,7 +31,7 @@ func (s *searchHTTPService) RegisterHTTPRoutes(storageRoute routing.RouteRegiste storageRoute.Post("/", middleware.ReqSignedIn, routing.Wrap(s.doQuery)) } -func (s *searchHTTPService) doQuery(c *models.ReqContext) response.Response { +func (s *searchHTTPService) doQuery(c *contextmodel.ReqContext) response.Response { searchReadinessCheckResp := s.search.IsReady(c.Req.Context(), c.OrgID) if !searchReadinessCheckResp.IsReady { dashboardSearchNotServedRequestsCounter.With(prometheus.Labels{ diff --git a/pkg/services/searchV2/index.go b/pkg/services/searchV2/index.go index b3efdb079ee..0c15395cd55 100644 --- a/pkg/services/searchV2/index.go +++ b/pkg/services/searchV2/index.go @@ -20,6 +20,7 @@ import ( "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/folder" "github.com/grafana/grafana/pkg/services/store" "github.com/grafana/grafana/pkg/services/store/entity" kdash "github.com/grafana/grafana/pkg/services/store/kind/dashboard" @@ -761,7 +762,7 @@ func (i *searchIndex) updateDashboard(ctx context.Context, orgID int64, index *o var folderUID string if dash.folderID == 0 { - folderUID = "general" + folderUID = folder.GeneralFolderUID } else { var err error folderUID, err = i.folderIdLookup(ctx, dash.folderID) @@ -901,22 +902,7 @@ func (l sqlDashboardLoader) LoadDashboards(ctx context.Context, orgID int64, das if dashboardUID == "" { limit = l.settings.DashboardLoadingBatchSize - dashboards = make([]dashboard, 0, limit+1) - - // Add the root folder ID (does not exist in SQL). - dashboards = append(dashboards, dashboard{ - id: 0, - uid: "", - isFolder: true, - folderID: 0, - slug: "", - created: time.Now(), - updated: time.Now(), - summary: &entity.EntitySummary{ - //ID: 0, - Name: "General", - }, - }) + dashboards = make([]dashboard, 0, limit) } loadDatasourceCtx, loadDatasourceSpan := l.tracer.Start(ctx, "sqlDashboardLoader LoadDatasourceLookup") diff --git a/pkg/services/searchV2/index_test.go b/pkg/services/searchV2/index_test.go index 431b43d6d6e..f0fa100dc91 100644 --- a/pkg/services/searchV2/index_test.go +++ b/pkg/services/searchV2/index_test.go @@ -6,20 +6,18 @@ import ( "path/filepath" "testing" + "github.com/blugelabs/bluge" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" - - "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/services/store/entity" - "github.com/grafana/grafana/pkg/setting" - - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/services/store" - - "github.com/blugelabs/bluge" "github.com/grafana/grafana-plugin-sdk-go/experimental" "github.com/stretchr/testify/require" + + "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/store" + "github.com/grafana/grafana/pkg/services/store/entity" + "github.com/grafana/grafana/pkg/setting" ) type testDashboardLoader struct { @@ -32,11 +30,11 @@ func (t *testDashboardLoader) LoadDashboards(_ context.Context, _ int64, _ strin var testLogger = log.New("index-test-logger") -var testAllowAllFilter = func(uid string) bool { +var testAllowAllFilter = func(kind entityKind, uid, parent string) bool { return true } -var testDisallowAllFilter = func(uid string) bool { +var testDisallowAllFilter = func(kind entityKind, uid, parent string) bool { return false } @@ -430,8 +428,8 @@ var dashboardsWithFolders = []dashboard{ summary: &entity.EntitySummary{ Name: "Dashboard in folder 1", Nested: []*entity.EntitySummary{ - newNestedPanel(1, "Panel 1"), - newNestedPanel(2, "Panel 2"), + newNestedPanel(1, 2, "Panel 1"), + newNestedPanel(2, 2, "Panel 2"), }, }, }, @@ -442,7 +440,7 @@ var dashboardsWithFolders = []dashboard{ summary: &entity.EntitySummary{ Name: "Dashboard in folder 2", Nested: []*entity.EntitySummary{ - newNestedPanel(3, "Panel 3"), + newNestedPanel(3, 3, "Panel 3"), }, }, }, @@ -452,7 +450,7 @@ var dashboardsWithFolders = []dashboard{ summary: &entity.EntitySummary{ Name: "One more dash", Nested: []*entity.EntitySummary{ - newNestedPanel(4, "Panel 4"), + newNestedPanel(4, 4, "Panel 4"), }, }, }, @@ -509,17 +507,17 @@ var dashboardsWithPanels = []dashboard{ summary: &entity.EntitySummary{ Name: "My Dash", Nested: []*entity.EntitySummary{ - newNestedPanel(1, "Panel 1"), - newNestedPanel(2, "Panel 2"), + newNestedPanel(1, 1, "Panel 1"), + newNestedPanel(2, 1, "Panel 2"), }, }, }, } -func newNestedPanel(id int64, name string) *entity.EntitySummary { +func newNestedPanel(id, dashId int64, name string) *entity.EntitySummary { summary := &entity.EntitySummary{ Kind: "panel", - UID: fmt.Sprintf("???#%d", id), + UID: fmt.Sprintf("%d#%d", dashId, id), } summary.Name = name return summary diff --git a/pkg/services/searchV2/queries.go b/pkg/services/searchV2/queries.go index 110f16f4e0e..e7104e26640 100644 --- a/pkg/services/searchV2/queries.go +++ b/pkg/services/searchV2/queries.go @@ -6,6 +6,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/services/querylibrary" "github.com/grafana/grafana/pkg/services/user" ) diff --git a/pkg/services/searchV2/search_service_mock.go b/pkg/services/searchV2/search_service_mock.go index 98b74731cc5..63f7214c067 100644 --- a/pkg/services/searchV2/search_service_mock.go +++ b/pkg/services/searchV2/search_service_mock.go @@ -6,7 +6,6 @@ import ( context "context" backend "github.com/grafana/grafana-plugin-sdk-go/backend" - mock "github.com/stretchr/testify/mock" user "github.com/grafana/grafana/pkg/services/user" diff --git a/pkg/services/searchV2/service.go b/pkg/services/searchV2/service.go index e510709172f..c8fe32896f6 100644 --- a/pkg/services/searchV2/service.go +++ b/pkg/services/searchV2/service.go @@ -10,8 +10,6 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" - "github.com/grafana/grafana/pkg/services/querylibrary" - "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" @@ -19,6 +17,7 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/querylibrary" "github.com/grafana/grafana/pkg/services/store" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" @@ -91,7 +90,7 @@ func ProvideService(cfg *setting.Cfg, sql db.DB, entityEventStore store.EntityEv cfg: cfg, sql: sql, ac: ac, - auth: &simpleSQLAuthService{ + auth: &simpleAuthService{ sql: sql, ac: ac, }, diff --git a/pkg/services/searchV2/service_bench_test.go b/pkg/services/searchV2/service_bench_test.go new file mode 100644 index 00000000000..989542d5aa7 --- /dev/null +++ b/pkg/services/searchV2/service_bench_test.go @@ -0,0 +1,211 @@ +package searchV2 + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/services/accesscontrol/actest" + "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/org/orgtest" + "github.com/grafana/grafana/pkg/services/querylibrary/querylibraryimpl" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/services/store" + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/setting" +) + +// setupBenchEnv will set up a database with folderCount folders and dashboardsPerFolder dashboards per folder +// It will also set up and run the search service +// and create a signed in user object with explicit permissions on each dashboard and folder. +func setupBenchEnv(b *testing.B, folderCount, dashboardsPerFolder int) (*StandardSearchService, *user.SignedInUser, error) { + sqlStore := db.InitTestDB(b) + err := populateDB(folderCount, dashboardsPerFolder, sqlStore) + require.NoError(b, err, "error when populating the database") + + // load all dashboards and folders + dbLoadingBatchSize := (dashboardsPerFolder + 1) * folderCount + cfg := &setting.Cfg{Search: setting.SearchSettings{DashboardLoadingBatchSize: dbLoadingBatchSize}} + features := featuremgmt.WithFeatures() + orgSvc := &orgtest.FakeOrgService{ + ExpectedOrgs: []*org.OrgDTO{{ID: 1}}, + } + querySvc := querylibraryimpl.ProvideService(cfg, features) + searchService, ok := ProvideService(cfg, sqlStore, store.NewDummyEntityEventsService(), actest.FakeService{}, + tracing.InitializeTracerForTest(), features, orgSvc, nil, querySvc).(*StandardSearchService) + require.True(b, ok) + + err = runSearchService(searchService) + require.NoError(b, err, "error when running search service") + + user := getSignedInUser(folderCount, dashboardsPerFolder) + + return searchService, user, nil +} + +// Returns a signed in user object with permissions on all dashboards and folders +func getSignedInUser(folderCount, dashboardsPerFolder int) *user.SignedInUser { + folderScopes := make([]string, folderCount) + for i := 1; i <= folderCount; i++ { + folderScopes[i-1] = dashboards.ScopeFoldersProvider.GetResourceScopeUID(fmt.Sprintf("folder%d", i)) + } + + dashScopes := make([]string, folderCount*dashboardsPerFolder) + for i := folderCount + 1; i <= (folderCount * (dashboardsPerFolder + 1)); i++ { + dashScopes[i-(folderCount+1)] = dashboards.ScopeDashboardsProvider.GetResourceScopeUID(fmt.Sprintf("dashboard%d", i)) + } + + user := &user.SignedInUser{ + UserID: 1, + OrgID: 1, + Permissions: map[int64]map[string][]string{ + 1: { + dashboards.ActionDashboardsRead: dashScopes, + dashboards.ActionFoldersRead: folderScopes, + }, + }, + } + + return user +} + +// Runs initial indexing of search service +func runSearchService(searchService *StandardSearchService) error { + if err := searchService.dashboardIndex.buildInitialIndexes(context.Background(), []int64{int64(1)}); err != nil { + return err + } + searchService.dashboardIndex.initialIndexingComplete = true + + // Required for sync that is called during dashboard search + go func() { + for { + doneCh := <-searchService.dashboardIndex.syncCh + close(doneCh) + } + }() + + return nil +} + +// Populates database with dashboards and folders +func populateDB(folderCount, dashboardsPerFolder int, sqlStore *sqlstore.SQLStore) error { + // Insert folders + offset := 1 + if errInsert := actest.ConcurrentBatch(actest.Concurrency, folderCount, actest.BatchSize, func(start, end int) error { + n := end - start + folders := make([]dashboards.Dashboard, 0, n) + now := time.Now() + + for u := start; u < end; u++ { + folderID := int64(u + offset) + folders = append(folders, dashboards.Dashboard{ + ID: folderID, + UID: fmt.Sprintf("folder%v", folderID), + Title: fmt.Sprintf("folder%v", folderID), + IsFolder: true, + OrgID: 1, + Created: now, + Updated: now, + }) + } + + err := sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error { + if _, err := sess.Insert(folders); err != nil { + return err + } + return nil + }) + return err + }); errInsert != nil { + return errInsert + } + + // Insert dashboards + offset += folderCount + if errInsert := actest.ConcurrentBatch(actest.Concurrency, dashboardsPerFolder*folderCount, actest.BatchSize, func(start, end int) error { + n := end - start + dbs := make([]dashboards.Dashboard, 0, n) + now := time.Now() + + for u := start; u < end; u++ { + dashID := int64(u + offset) + folderID := int64((u+offset)%folderCount + 1) + dbs = append(dbs, dashboards.Dashboard{ + ID: dashID, + UID: fmt.Sprintf("dashboard%v", dashID), + Title: fmt.Sprintf("dashboard%v", dashID), + IsFolder: false, + FolderID: folderID, + OrgID: 1, + Created: now, + Updated: now, + }) + } + + err := sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error { + if _, err := sess.Insert(dbs); err != nil { + return err + } + return nil + }) + return err + }); errInsert != nil { + return errInsert + } + + return nil +} + +func benchSearchV2(b *testing.B, folderCount, dashboardsPerFolder int) { + searchService, testUser, err := setupBenchEnv(b, folderCount, dashboardsPerFolder) + require.NoError(b, err) + + b.ResetTimer() + + expectedResultCount := (dashboardsPerFolder + 1) * folderCount + for n := 0; n < b.N; n++ { + result := searchService.doDashboardQuery(context.Background(), testUser, 1, DashboardQuery{Limit: expectedResultCount}) + require.NoError(b, result.Error) + require.NotZero(b, len(result.Frames)) + for _, field := range result.Frames[0].Fields { + if field.Name == "uid" { + require.Equal(b, expectedResultCount, field.Len()) + break + } + } + } +} + +// Test with some dashboards and some folders +func BenchmarkSearchV2_10_10(b *testing.B) { + benchSearchV2(b, 10, 10) +} // ~0.0002 s/op +func BenchmarkSearchV2_10_100(b *testing.B) { + benchSearchV2(b, 10, 100) +} // ~0.002 s/op + +// Test with many dashboards and only one folder +func BenchmarkSearchV2_1_1k(b *testing.B) { + benchSearchV2(b, 1, 1000) +} // ~0.002 s/op +func BenchmarkSearchV2_1_10k(b *testing.B) { + benchSearchV2(b, 1, 10000) +} // ~0.019 s/op + +// Test with a large number of dashboards and folders +func BenchmarkSearchV2_100_100(b *testing.B) { + benchSearchV2(b, 100, 100) +} // ~0.02 s/op +func BenchmarkSearchV2_100_1k(b *testing.B) { + benchSearchV2(b, 100, 1000) +} // ~0.22 s/op +func BenchmarkSearchV2_1k_100(b *testing.B) { + benchSearchV2(b, 1000, 100) +} // ~0.22 s/op diff --git a/pkg/services/searchV2/stub.go b/pkg/services/searchV2/stub.go index b3930c16c50..f3edd473fe6 100644 --- a/pkg/services/searchV2/stub.go +++ b/pkg/services/searchV2/stub.go @@ -5,6 +5,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/services/user" ) diff --git a/pkg/services/searchV2/testdata/basic-search.jsonc b/pkg/services/searchV2/testdata/basic-search.jsonc index 0d1784474ed..75d20de5933 100644 --- a/pkg/services/searchV2/testdata/basic-search.jsonc +++ b/pkg/services/searchV2/testdata/basic-search.jsonc @@ -13,7 +13,7 @@ // | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | // | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string | // +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+ -// | dashboard | 2 | boom | | /pfix/d/2/ | null | [] | | +// | dashboard | 2 | boom | | /pfix/d/2/ | null | [] | general | // +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+ // // @@ -122,7 +122,7 @@ [] ], [ - "" + "general" ] ] } diff --git a/pkg/services/searchV2/testdata/folders-dashboard-removed-on-folder-removed.jsonc b/pkg/services/searchV2/testdata/folders-dashboard-removed-on-folder-removed.jsonc index 019e0ba2b28..29082357672 100644 --- a/pkg/services/searchV2/testdata/folders-dashboard-removed-on-folder-removed.jsonc +++ b/pkg/services/searchV2/testdata/folders-dashboard-removed-on-folder-removed.jsonc @@ -13,7 +13,7 @@ // | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | // | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string | // +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+ -// | dashboard | 4 | One more dash | | /pfix/d/4/ | null | [] | | +// | dashboard | 4 | One more dash | | /pfix/d/4/ | null | [] | general | // +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+ // // @@ -122,7 +122,7 @@ [] ], [ - "" + "general" ] ] } diff --git a/pkg/services/searchV2/testdata/multiple-tokens-beginning-lower.jsonc b/pkg/services/searchV2/testdata/multiple-tokens-beginning-lower.jsonc index 4536d5a36a0..3f1af3821bd 100644 --- a/pkg/services/searchV2/testdata/multiple-tokens-beginning-lower.jsonc +++ b/pkg/services/searchV2/testdata/multiple-tokens-beginning-lower.jsonc @@ -13,7 +13,7 @@ // | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | // | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string | // +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+ -// | dashboard | 1 | Archer Data System | | /pfix/d/1/ | null | [] | | +// | dashboard | 1 | Archer Data System | | /pfix/d/1/ | null | [] | general | // +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+ // // @@ -122,7 +122,7 @@ [] ], [ - "" + "general" ] ] } diff --git a/pkg/services/searchV2/testdata/multiple-tokens-beginning.jsonc b/pkg/services/searchV2/testdata/multiple-tokens-beginning.jsonc index 4536d5a36a0..3f1af3821bd 100644 --- a/pkg/services/searchV2/testdata/multiple-tokens-beginning.jsonc +++ b/pkg/services/searchV2/testdata/multiple-tokens-beginning.jsonc @@ -13,7 +13,7 @@ // | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | // | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string | // +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+ -// | dashboard | 1 | Archer Data System | | /pfix/d/1/ | null | [] | | +// | dashboard | 1 | Archer Data System | | /pfix/d/1/ | null | [] | general | // +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+ // // @@ -122,7 +122,7 @@ [] ], [ - "" + "general" ] ] } diff --git a/pkg/services/searchV2/testdata/multiple-tokens-middle-lower.jsonc b/pkg/services/searchV2/testdata/multiple-tokens-middle-lower.jsonc index 4102a2ad602..87e8e460095 100644 --- a/pkg/services/searchV2/testdata/multiple-tokens-middle-lower.jsonc +++ b/pkg/services/searchV2/testdata/multiple-tokens-middle-lower.jsonc @@ -13,7 +13,7 @@ // | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | // | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string | // +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+ -// | dashboard | 2 | Document Sync repo | | /pfix/d/2/ | null | [] | | +// | dashboard | 2 | Document Sync repo | | /pfix/d/2/ | null | [] | general | // +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+ // // @@ -122,7 +122,7 @@ [] ], [ - "" + "general" ] ] } diff --git a/pkg/services/searchV2/testdata/multiple-tokens-middle.jsonc b/pkg/services/searchV2/testdata/multiple-tokens-middle.jsonc index 4536d5a36a0..3f1af3821bd 100644 --- a/pkg/services/searchV2/testdata/multiple-tokens-middle.jsonc +++ b/pkg/services/searchV2/testdata/multiple-tokens-middle.jsonc @@ -13,7 +13,7 @@ // | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | // | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string | // +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+ -// | dashboard | 1 | Archer Data System | | /pfix/d/1/ | null | [] | | +// | dashboard | 1 | Archer Data System | | /pfix/d/1/ | null | [] | general | // +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+ // // @@ -122,7 +122,7 @@ [] ], [ - "" + "general" ] ] } diff --git a/pkg/services/searchV2/testdata/ngram-camel-case-split.jsonc b/pkg/services/searchV2/testdata/ngram-camel-case-split.jsonc index 8a43418872a..c49bf638f3c 100644 --- a/pkg/services/searchV2/testdata/ngram-camel-case-split.jsonc +++ b/pkg/services/searchV2/testdata/ngram-camel-case-split.jsonc @@ -13,7 +13,7 @@ // | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | // | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string | // +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+ -// | dashboard | 1 | heatTorkel | | /pfix/d/1/ | null | [] | | +// | dashboard | 1 | heatTorkel | | /pfix/d/1/ | null | [] | general | // +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+ // // @@ -122,7 +122,7 @@ [] ], [ - "" + "general" ] ] } diff --git a/pkg/services/searchV2/testdata/ngram-punctuation-split.jsonc b/pkg/services/searchV2/testdata/ngram-punctuation-split.jsonc index a6501a132b3..bf363db06c3 100644 --- a/pkg/services/searchV2/testdata/ngram-punctuation-split.jsonc +++ b/pkg/services/searchV2/testdata/ngram-punctuation-split.jsonc @@ -13,7 +13,7 @@ // | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | // | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string | // +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+ -// | dashboard | 1 | heat-torkel | | /pfix/d/1/ | null | [] | | +// | dashboard | 1 | heat-torkel | | /pfix/d/1/ | null | [] | general | // +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+ // // @@ -122,7 +122,7 @@ [] ], [ - "" + "general" ] ] } diff --git a/pkg/services/searchV2/testdata/ngram-simple.jsonc b/pkg/services/searchV2/testdata/ngram-simple.jsonc index 0a85bf090b1..a9b65c0e829 100644 --- a/pkg/services/searchV2/testdata/ngram-simple.jsonc +++ b/pkg/services/searchV2/testdata/ngram-simple.jsonc @@ -13,8 +13,8 @@ // | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | // | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string | // +----------------+----------------+------------------+------------------+----------------+--------------------------+-------------------------+----------------+ -// | dashboard | 1 | heat-torkel | | /pfix/d/1/ | null | [] | | -// | dashboard | 2 | topology heatmap | | /pfix/d/2/ | null | [] | | +// | dashboard | 1 | heat-torkel | | /pfix/d/1/ | null | [] | general | +// | dashboard | 2 | topology heatmap | | /pfix/d/2/ | null | [] | general | // +----------------+----------------+------------------+------------------+----------------+--------------------------+-------------------------+----------------+ // // @@ -130,8 +130,8 @@ [] ], [ - "", - "" + "general", + "general" ] ] } diff --git a/pkg/services/searchV2/testdata/prefix-search-beginning-lower.jsonc b/pkg/services/searchV2/testdata/prefix-search-beginning-lower.jsonc index 4536d5a36a0..3f1af3821bd 100644 --- a/pkg/services/searchV2/testdata/prefix-search-beginning-lower.jsonc +++ b/pkg/services/searchV2/testdata/prefix-search-beginning-lower.jsonc @@ -13,7 +13,7 @@ // | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | // | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string | // +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+ -// | dashboard | 1 | Archer Data System | | /pfix/d/1/ | null | [] | | +// | dashboard | 1 | Archer Data System | | /pfix/d/1/ | null | [] | general | // +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+ // // @@ -122,7 +122,7 @@ [] ], [ - "" + "general" ] ] } diff --git a/pkg/services/searchV2/testdata/prefix-search-beginning.jsonc b/pkg/services/searchV2/testdata/prefix-search-beginning.jsonc index 4536d5a36a0..3f1af3821bd 100644 --- a/pkg/services/searchV2/testdata/prefix-search-beginning.jsonc +++ b/pkg/services/searchV2/testdata/prefix-search-beginning.jsonc @@ -13,7 +13,7 @@ // | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | // | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string | // +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+ -// | dashboard | 1 | Archer Data System | | /pfix/d/1/ | null | [] | | +// | dashboard | 1 | Archer Data System | | /pfix/d/1/ | null | [] | general | // +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+ // // @@ -122,7 +122,7 @@ [] ], [ - "" + "general" ] ] } diff --git a/pkg/services/searchV2/testdata/prefix-search-middle-lower.jsonc b/pkg/services/searchV2/testdata/prefix-search-middle-lower.jsonc index 4102a2ad602..87e8e460095 100644 --- a/pkg/services/searchV2/testdata/prefix-search-middle-lower.jsonc +++ b/pkg/services/searchV2/testdata/prefix-search-middle-lower.jsonc @@ -13,7 +13,7 @@ // | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | // | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string | // +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+ -// | dashboard | 2 | Document Sync repo | | /pfix/d/2/ | null | [] | | +// | dashboard | 2 | Document Sync repo | | /pfix/d/2/ | null | [] | general | // +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+ // // @@ -122,7 +122,7 @@ [] ], [ - "" + "general" ] ] } diff --git a/pkg/services/searchV2/testdata/prefix-search-middle.jsonc b/pkg/services/searchV2/testdata/prefix-search-middle.jsonc index 4102a2ad602..87e8e460095 100644 --- a/pkg/services/searchV2/testdata/prefix-search-middle.jsonc +++ b/pkg/services/searchV2/testdata/prefix-search-middle.jsonc @@ -13,7 +13,7 @@ // | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | // | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string | // +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+ -// | dashboard | 2 | Document Sync repo | | /pfix/d/2/ | null | [] | | +// | dashboard | 2 | Document Sync repo | | /pfix/d/2/ | null | [] | general | // +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+ // // @@ -122,7 +122,7 @@ [] ], [ - "" + "general" ] ] } diff --git a/pkg/services/searchV2/testdata/prefix-search-ngram-exceeded.jsonc b/pkg/services/searchV2/testdata/prefix-search-ngram-exceeded.jsonc index 2e64a8cb02d..b09b73885d4 100644 --- a/pkg/services/searchV2/testdata/prefix-search-ngram-exceeded.jsonc +++ b/pkg/services/searchV2/testdata/prefix-search-ngram-exceeded.jsonc @@ -13,7 +13,7 @@ // | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | // | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string | // +----------------+----------------+--------------------------------+------------------+----------------+--------------------------+-------------------------+----------------+ -// | dashboard | 1 | Eyjafjallajökull Eruption data | | /pfix/d/1/ | null | [] | | +// | dashboard | 1 | Eyjafjallajökull Eruption data | | /pfix/d/1/ | null | [] | general | // +----------------+----------------+--------------------------------+------------------+----------------+--------------------------+-------------------------+----------------+ // // @@ -122,7 +122,7 @@ [] ], [ - "" + "general" ] ] } diff --git a/pkg/services/searchV2/testdata/scattered-tokens-match.jsonc b/pkg/services/searchV2/testdata/scattered-tokens-match.jsonc index 19404b6a481..84be3a0a752 100644 --- a/pkg/services/searchV2/testdata/scattered-tokens-match.jsonc +++ b/pkg/services/searchV2/testdata/scattered-tokens-match.jsonc @@ -13,7 +13,7 @@ // | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | // | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string | // +----------------+----------------+----------------------------------------------------------------------+------------------+----------------+--------------------------+-------------------------+----------------+ -// | dashboard | 1 | Three can keep a secret, if two of them are dead (Benjamin Franklin) | | /pfix/d/1/ | null | [] | | +// | dashboard | 1 | Three can keep a secret, if two of them are dead (Benjamin Franklin) | | /pfix/d/1/ | null | [] | general | // +----------------+----------------+----------------------------------------------------------------------+------------------+----------------+--------------------------+-------------------------+----------------+ // // @@ -122,7 +122,7 @@ [] ], [ - "" + "general" ] ] } diff --git a/pkg/services/searchV2/testdata/sort-asc.jsonc b/pkg/services/searchV2/testdata/sort-asc.jsonc index a284a955e7f..c53caec66f3 100644 --- a/pkg/services/searchV2/testdata/sort-asc.jsonc +++ b/pkg/services/searchV2/testdata/sort-asc.jsonc @@ -14,8 +14,8 @@ // | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | // | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string | Type: []float64 | // +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+-----------------+ -// | dashboard | 1 | a-test | | /pfix/d/1/ | null | [] | | 0 | -// | dashboard | 2 | z-test | | /pfix/d/2/ | null | [] | | 1 | +// | dashboard | 1 | a-test | | /pfix/d/1/ | null | [] | general | 0 | +// | dashboard | 2 | z-test | | /pfix/d/2/ | null | [] | general | 1 | // +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+-----------------+ // // @@ -139,8 +139,8 @@ [] ], [ - "", - "" + "general", + "general" ], [ 0, diff --git a/pkg/services/searchV2/testdata/sort-desc.jsonc b/pkg/services/searchV2/testdata/sort-desc.jsonc index 998c5cf254d..540d0e82b62 100644 --- a/pkg/services/searchV2/testdata/sort-desc.jsonc +++ b/pkg/services/searchV2/testdata/sort-desc.jsonc @@ -14,8 +14,8 @@ // | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | // | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string | Type: []float64 | // +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+-----------------+ -// | dashboard | 2 | z-test | | /pfix/d/2/ | null | [] | | 3 | -// | dashboard | 1 | a-test | | /pfix/d/1/ | null | [] | | 2 | +// | dashboard | 2 | z-test | | /pfix/d/2/ | null | [] | general | 3 | +// | dashboard | 1 | a-test | | /pfix/d/1/ | null | [] | general | 2 | // +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+-----------------+ // // @@ -139,8 +139,8 @@ [] ], [ - "", - "" + "general", + "general" ], [ 3, diff --git a/pkg/services/searchV2/types.go b/pkg/services/searchV2/types.go index 95b7a937aab..2c6b7fcd726 100644 --- a/pkg/services/searchV2/types.go +++ b/pkg/services/searchV2/types.go @@ -3,10 +3,10 @@ package searchV2 import ( "context" + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/services/user" - - "github.com/grafana/grafana-plugin-sdk-go/backend" ) type FacetField struct { diff --git a/pkg/services/searchV2/usage.go b/pkg/services/searchV2/usage.go index 8fd2caaefd0..fe325e084b6 100644 --- a/pkg/services/searchV2/usage.go +++ b/pkg/services/searchV2/usage.go @@ -6,10 +6,11 @@ import ( "github.com/blugelabs/bluge" "github.com/blugelabs/bluge/search" "github.com/blugelabs/bluge/search/aggregations" - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/infra/tracing" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/tracing" ) type usageGauge struct { diff --git a/pkg/services/searchusers/searchusers.go b/pkg/services/searchusers/searchusers.go index 4e57c678036..bd82503d691 100644 --- a/pkg/services/searchusers/searchusers.go +++ b/pkg/services/searchusers/searchusers.go @@ -5,14 +5,14 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/user" ) type Service interface { - SearchUsers(c *models.ReqContext) response.Response - SearchUsersWithPaging(c *models.ReqContext) response.Response + SearchUsers(c *contextmodel.ReqContext) response.Response + SearchUsersWithPaging(c *contextmodel.ReqContext) response.Response } type OSSService struct { @@ -39,7 +39,7 @@ func ProvideUsersService(searchUserFilter user.SearchUserFilter, userService use // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (s *OSSService) SearchUsers(c *models.ReqContext) response.Response { +func (s *OSSService) SearchUsers(c *contextmodel.ReqContext) response.Response { result, err := s.SearchUser(c) if err != nil { return response.Error(500, "Failed to fetch users", err) @@ -58,7 +58,7 @@ func (s *OSSService) SearchUsers(c *models.ReqContext) response.Response { // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (s *OSSService) SearchUsersWithPaging(c *models.ReqContext) response.Response { +func (s *OSSService) SearchUsersWithPaging(c *contextmodel.ReqContext) response.Response { result, err := s.SearchUser(c) if err != nil { return response.Error(500, "Failed to fetch users", err) @@ -67,7 +67,7 @@ func (s *OSSService) SearchUsersWithPaging(c *models.ReqContext) response.Respon return response.JSON(http.StatusOK, result) } -func (s *OSSService) SearchUser(c *models.ReqContext) (*user.SearchUserQueryResult, error) { +func (s *OSSService) SearchUser(c *contextmodel.ReqContext) (*user.SearchUserQueryResult, error) { perPage := c.QueryInt("perpage") if perPage <= 0 { perPage = 1000 diff --git a/pkg/services/secrets/kvstore/plugin_test.go b/pkg/services/secrets/kvstore/plugin_test.go index 19af6217f29..460adea650d 100644 --- a/pkg/services/secrets/kvstore/plugin_test.go +++ b/pkg/services/secrets/kvstore/plugin_test.go @@ -4,9 +4,10 @@ import ( "context" "testing" - "github.com/grafana/grafana/pkg/plugins/backendplugin/secretsmanagerplugin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/plugins/backendplugin/secretsmanagerplugin" ) // Set fatal flag to true, then simulate a plugin start failure diff --git a/pkg/services/secrets/manager/helpers.go b/pkg/services/secrets/manager/helpers.go index 967c909be33..e063c38046d 100644 --- a/pkg/services/secrets/manager/helpers.go +++ b/pkg/services/secrets/manager/helpers.go @@ -3,6 +3,9 @@ package manager import ( "testing" + "github.com/stretchr/testify/require" + "gopkg.in/ini.v1" + "github.com/grafana/grafana/pkg/infra/usagestats" encryptionprovider "github.com/grafana/grafana/pkg/services/encryption/provider" encryptionservice "github.com/grafana/grafana/pkg/services/encryption/service" @@ -10,8 +13,6 @@ import ( "github.com/grafana/grafana/pkg/services/kmsproviders/osskmsproviders" "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/require" - "gopkg.in/ini.v1" ) func SetupTestService(tb testing.TB, store secrets.Store) *SecretsService { diff --git a/pkg/services/secrets/manager/manager.go b/pkg/services/secrets/manager/manager.go index c9c5c2b9967..6e0180a2454 100644 --- a/pkg/services/secrets/manager/manager.go +++ b/pkg/services/secrets/manager/manager.go @@ -11,6 +11,9 @@ import ( "sync" "time" + "github.com/prometheus/client_golang/prometheus" + "golang.org/x/sync/errgroup" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/services/encryption" @@ -19,9 +22,6 @@ import ( "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" - - "github.com/prometheus/client_golang/prometheus" - "golang.org/x/sync/errgroup" ) const ( diff --git a/pkg/services/secrets/manager/metrics.go b/pkg/services/secrets/manager/metrics.go index b435f71036d..669d4c7a1cc 100644 --- a/pkg/services/secrets/manager/metrics.go +++ b/pkg/services/secrets/manager/metrics.go @@ -1,9 +1,10 @@ package manager import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/infra/metrics/metricutil" - "github.com/prometheus/client_golang/prometheus" ) const ( diff --git a/pkg/services/serviceaccounts/api/api.go b/pkg/services/serviceaccounts/api/api.go index fc20c400a3a..d48ee7efb1c 100644 --- a/pkg/services/serviceaccounts/api/api.go +++ b/pkg/services/serviceaccounts/api/api.go @@ -11,9 +11,9 @@ import ( "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/apikey" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/grafana/grafana/pkg/services/serviceaccounts/database" @@ -117,7 +117,7 @@ func (api *ServiceAccountsAPI) RegisterAPIEndpoints() { // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (api *ServiceAccountsAPI) CreateServiceAccount(c *models.ReqContext) response.Response { +func (api *ServiceAccountsAPI) CreateServiceAccount(c *contextmodel.ReqContext) response.Response { cmd := serviceaccounts.CreateServiceAccountForm{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "Bad request data", err) @@ -171,7 +171,7 @@ func (api *ServiceAccountsAPI) CreateServiceAccount(c *models.ReqContext) respon // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (api *ServiceAccountsAPI) RetrieveServiceAccount(ctx *models.ReqContext) response.Response { +func (api *ServiceAccountsAPI) RetrieveServiceAccount(ctx *contextmodel.ReqContext) response.Response { scopeID, err := strconv.ParseInt(web.Params(ctx.Req)[":serviceAccountId"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "Service Account ID is invalid", err) @@ -218,7 +218,7 @@ func (api *ServiceAccountsAPI) RetrieveServiceAccount(ctx *models.ReqContext) re // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (api *ServiceAccountsAPI) UpdateServiceAccount(c *models.ReqContext) response.Response { +func (api *ServiceAccountsAPI) UpdateServiceAccount(c *contextmodel.ReqContext) response.Response { scopeID, err := strconv.ParseInt(web.Params(c.Req)[":serviceAccountId"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "Service Account ID is invalid", err) @@ -286,7 +286,7 @@ func (api *ServiceAccountsAPI) validateRole(r *org.RoleType, orgRole *org.RoleTy // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (api *ServiceAccountsAPI) DeleteServiceAccount(ctx *models.ReqContext) response.Response { +func (api *ServiceAccountsAPI) DeleteServiceAccount(ctx *contextmodel.ReqContext) response.Response { scopeID, err := strconv.ParseInt(web.Params(ctx.Req)[":serviceAccountId"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "Service account ID is invalid", err) @@ -310,7 +310,7 @@ func (api *ServiceAccountsAPI) DeleteServiceAccount(ctx *models.ReqContext) resp // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (api *ServiceAccountsAPI) SearchOrgServiceAccountsWithPaging(c *models.ReqContext) response.Response { +func (api *ServiceAccountsAPI) SearchOrgServiceAccountsWithPaging(c *contextmodel.ReqContext) response.Response { ctx := c.Req.Context() perPage := c.QueryInt("perpage") if perPage <= 0 { @@ -365,7 +365,7 @@ func (api *ServiceAccountsAPI) SearchOrgServiceAccountsWithPaging(c *models.ReqC } // GET /api/serviceaccounts/migrationstatus -func (api *ServiceAccountsAPI) GetAPIKeysMigrationStatus(ctx *models.ReqContext) response.Response { +func (api *ServiceAccountsAPI) GetAPIKeysMigrationStatus(ctx *contextmodel.ReqContext) response.Response { upgradeStatus, err := api.service.GetAPIKeysMigrationStatus(ctx.Req.Context(), ctx.OrgID) if err != nil { return response.Error(http.StatusInternalServerError, "Internal server error", err) @@ -374,7 +374,7 @@ func (api *ServiceAccountsAPI) GetAPIKeysMigrationStatus(ctx *models.ReqContext) } // POST /api/serviceaccounts/hideapikeys -func (api *ServiceAccountsAPI) HideApiKeysTab(ctx *models.ReqContext) response.Response { +func (api *ServiceAccountsAPI) HideApiKeysTab(ctx *contextmodel.ReqContext) response.Response { if err := api.service.HideApiKeysTab(ctx.Req.Context(), ctx.OrgID); err != nil { return response.Error(http.StatusInternalServerError, "Internal server error", err) } @@ -382,7 +382,7 @@ func (api *ServiceAccountsAPI) HideApiKeysTab(ctx *models.ReqContext) response.R } // POST /api/serviceaccounts/migrate -func (api *ServiceAccountsAPI) MigrateApiKeysToServiceAccounts(ctx *models.ReqContext) response.Response { +func (api *ServiceAccountsAPI) MigrateApiKeysToServiceAccounts(ctx *contextmodel.ReqContext) response.Response { if err := api.service.MigrateApiKeysToServiceAccounts(ctx.Req.Context(), ctx.OrgID); err != nil { return response.Error(http.StatusInternalServerError, "Internal server error", err) } @@ -391,7 +391,7 @@ func (api *ServiceAccountsAPI) MigrateApiKeysToServiceAccounts(ctx *models.ReqCo } // POST /api/serviceaccounts/migrate/:keyId -func (api *ServiceAccountsAPI) ConvertToServiceAccount(ctx *models.ReqContext) response.Response { +func (api *ServiceAccountsAPI) ConvertToServiceAccount(ctx *contextmodel.ReqContext) response.Response { keyId, err := strconv.ParseInt(web.Params(ctx.Req)[":keyId"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "Key ID is invalid", err) @@ -405,7 +405,7 @@ func (api *ServiceAccountsAPI) ConvertToServiceAccount(ctx *models.ReqContext) r } // POST /api/serviceaccounts/revert/:keyId -func (api *ServiceAccountsAPI) RevertApiKey(ctx *models.ReqContext) response.Response { +func (api *ServiceAccountsAPI) RevertApiKey(ctx *contextmodel.ReqContext) response.Response { keyId, err := strconv.ParseInt(web.Params(ctx.Req)[":keyId"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "key ID is invalid", err) @@ -421,7 +421,7 @@ func (api *ServiceAccountsAPI) RevertApiKey(ctx *models.ReqContext) response.Res return response.Success("reverted service account to API key") } -func (api *ServiceAccountsAPI) getAccessControlMetadata(c *models.ReqContext, saIDs map[string]bool) map[string]accesscontrol.Metadata { +func (api *ServiceAccountsAPI) getAccessControlMetadata(c *contextmodel.ReqContext, saIDs map[string]bool) map[string]accesscontrol.Metadata { if api.accesscontrol.IsDisabled() || !c.QueryBool("accesscontrol") { return map[string]accesscontrol.Metadata{} } diff --git a/pkg/services/serviceaccounts/api/token.go b/pkg/services/serviceaccounts/api/token.go index 6be829ccad5..5867cc7734f 100644 --- a/pkg/services/serviceaccounts/api/token.go +++ b/pkg/services/serviceaccounts/api/token.go @@ -9,8 +9,8 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" apikeygenprefix "github.com/grafana/grafana/pkg/components/apikeygenprefixed" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/apikey" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/grafana/grafana/pkg/services/serviceaccounts/database" "github.com/grafana/grafana/pkg/web" @@ -66,7 +66,7 @@ const sevenDaysAhead = 7 * 24 * time.Hour // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (api *ServiceAccountsAPI) ListTokens(ctx *models.ReqContext) response.Response { +func (api *ServiceAccountsAPI) ListTokens(ctx *contextmodel.ReqContext) response.Response { saID, err := strconv.ParseInt(web.Params(ctx.Req)[":serviceAccountId"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "Service Account ID is invalid", err) @@ -127,7 +127,7 @@ func (api *ServiceAccountsAPI) ListTokens(ctx *models.ReqContext) response.Respo // 404: notFoundError // 409: conflictError // 500: internalServerError -func (api *ServiceAccountsAPI) CreateToken(c *models.ReqContext) response.Response { +func (api *ServiceAccountsAPI) CreateToken(c *contextmodel.ReqContext) response.Response { saID, err := strconv.ParseInt(web.Params(c.Req)[":serviceAccountId"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "Service Account ID is invalid", err) @@ -210,7 +210,7 @@ func (api *ServiceAccountsAPI) CreateToken(c *models.ReqContext) response.Respon // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (api *ServiceAccountsAPI) DeleteToken(c *models.ReqContext) response.Response { +func (api *ServiceAccountsAPI) DeleteToken(c *contextmodel.ReqContext) response.Response { saID, err := strconv.ParseInt(web.Params(c.Req)[":serviceAccountId"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "Service Account ID is invalid", err) diff --git a/pkg/services/serviceaccounts/database/stats_test.go b/pkg/services/serviceaccounts/database/stats_test.go index 6050ff70154..15f6983b138 100644 --- a/pkg/services/serviceaccounts/database/stats_test.go +++ b/pkg/services/serviceaccounts/database/stats_test.go @@ -4,11 +4,12 @@ import ( "context" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/components/apikeygen" "github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/grafana/grafana/pkg/services/serviceaccounts/tests" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestStore_UsageStats(t *testing.T) { diff --git a/pkg/services/serviceaccounts/database/token_store_test.go b/pkg/services/serviceaccounts/database/token_store_test.go index d06011139fb..c95cd1cd9ff 100644 --- a/pkg/services/serviceaccounts/database/token_store_test.go +++ b/pkg/services/serviceaccounts/database/token_store_test.go @@ -4,11 +4,12 @@ import ( "context" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/components/apikeygen" "github.com/grafana/grafana/pkg/services/apikey" "github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/grafana/grafana/pkg/services/serviceaccounts/tests" - "github.com/stretchr/testify/require" ) func TestStore_AddServiceAccountToken(t *testing.T) { diff --git a/pkg/services/serviceaccounts/manager/service_test.go b/pkg/services/serviceaccounts/manager/service_test.go index 916e825ca67..a5425acd7ad 100644 --- a/pkg/services/serviceaccounts/manager/service_test.go +++ b/pkg/services/serviceaccounts/manager/service_test.go @@ -4,12 +4,12 @@ import ( "context" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/apikey" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/serviceaccounts" - - "github.com/stretchr/testify/require" ) type FakeServiceAccountStore struct { diff --git a/pkg/services/serviceaccounts/manager/stats_test.go b/pkg/services/serviceaccounts/manager/stats_test.go index 4b4b2dd6ff1..a7b21041303 100644 --- a/pkg/services/serviceaccounts/manager/stats_test.go +++ b/pkg/services/serviceaccounts/manager/stats_test.go @@ -4,10 +4,11 @@ import ( "context" "testing" - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/serviceaccounts" ) func Test_UsageStats(t *testing.T) { diff --git a/pkg/services/serviceaccounts/secretscan/service_test.go b/pkg/services/serviceaccounts/secretscan/service_test.go index 1bb61b800e0..824b3f2e5e6 100644 --- a/pkg/services/serviceaccounts/secretscan/service_test.go +++ b/pkg/services/serviceaccounts/secretscan/service_test.go @@ -4,10 +4,11 @@ import ( "context" "testing" - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/services/apikey" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/apikey" ) func TestService_CheckTokens(t *testing.T) { diff --git a/pkg/services/sqlstore/logger.go b/pkg/services/sqlstore/logger.go index 850e10cd214..199a36f1d13 100644 --- a/pkg/services/sqlstore/logger.go +++ b/pkg/services/sqlstore/logger.go @@ -3,9 +3,9 @@ package sqlstore import ( "fmt" - glog "github.com/grafana/grafana/pkg/infra/log" - "xorm.io/core" + + glog "github.com/grafana/grafana/pkg/infra/log" ) type XormLogger struct { diff --git a/pkg/services/sqlstore/migrations/accesscontrol/action_migrator.go b/pkg/services/sqlstore/migrations/accesscontrol/action_migrator.go index 47de5f8fa30..b5c2ffffa3d 100644 --- a/pkg/services/sqlstore/migrations/accesscontrol/action_migrator.go +++ b/pkg/services/sqlstore/migrations/accesscontrol/action_migrator.go @@ -4,10 +4,10 @@ import ( "fmt" "strings" + "xorm.io/xorm" + "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" - - "xorm.io/xorm" ) const ActionMigrationID = "RBAC action name migrator" diff --git a/pkg/services/sqlstore/migrations/accesscontrol/managed_permission_migrator.go b/pkg/services/sqlstore/migrations/accesscontrol/managed_permission_migrator.go index 2a2a285c2c0..87f742ed38c 100644 --- a/pkg/services/sqlstore/migrations/accesscontrol/managed_permission_migrator.go +++ b/pkg/services/sqlstore/migrations/accesscontrol/managed_permission_migrator.go @@ -12,13 +12,14 @@ import ( "strings" "time" + "golang.org/x/text/cases" + "golang.org/x/text/language" + "xorm.io/xorm" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" - "golang.org/x/text/cases" - "golang.org/x/text/language" - "xorm.io/xorm" ) const ManagedPermissionsMigrationID = "managed permissions migration" diff --git a/pkg/services/sqlstore/migrations/accesscontrol/test/ac_test.go b/pkg/services/sqlstore/migrations/accesscontrol/test/ac_test.go index 0501f62580c..0c5ae95ae3d 100644 --- a/pkg/services/sqlstore/migrations/accesscontrol/test/ac_test.go +++ b/pkg/services/sqlstore/migrations/accesscontrol/test/ac_test.go @@ -6,6 +6,8 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "xorm.io/xorm" "github.com/grafana/grafana/pkg/infra/log" @@ -19,9 +21,6 @@ import ( "github.com/grafana/grafana/pkg/services/team" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) type rawPermission struct { diff --git a/pkg/services/sqlstore/migrations/accesscontrol/test/action_migrator_test.go b/pkg/services/sqlstore/migrations/accesscontrol/test/action_migrator_test.go index c3d78009a8b..ff0f3174aa7 100644 --- a/pkg/services/sqlstore/migrations/accesscontrol/test/action_migrator_test.go +++ b/pkg/services/sqlstore/migrations/accesscontrol/test/action_migrator_test.go @@ -4,15 +4,15 @@ import ( "fmt" "testing" - "github.com/grafana/grafana/pkg/services/dashboards" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/dashboards" acmig "github.com/grafana/grafana/pkg/services/sqlstore/migrations/accesscontrol" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/require" ) func TestActionMigration(t *testing.T) { diff --git a/pkg/services/sqlstore/migrations/accesscontrol/test/managed_permission_migrator_test.go b/pkg/services/sqlstore/migrations/accesscontrol/test/managed_permission_migrator_test.go index e8dd6d728c6..94ce65f9ee7 100644 --- a/pkg/services/sqlstore/migrations/accesscontrol/test/managed_permission_migrator_test.go +++ b/pkg/services/sqlstore/migrations/accesscontrol/test/managed_permission_migrator_test.go @@ -6,13 +6,14 @@ import ( "strings" "testing" + "github.com/stretchr/testify/require" + "xorm.io/xorm" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/accesscontrol" acmig "github.com/grafana/grafana/pkg/services/sqlstore/migrations/accesscontrol" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/require" - "xorm.io/xorm" ) type inheritanceTestCase struct { diff --git a/pkg/services/sqlstore/migrations/annotation_mig.go b/pkg/services/sqlstore/migrations/annotation_mig.go index f66a63f06c0..dd23ffea93d 100644 --- a/pkg/services/sqlstore/migrations/annotation_mig.go +++ b/pkg/services/sqlstore/migrations/annotation_mig.go @@ -1,8 +1,9 @@ package migrations import ( - . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "xorm.io/xorm" + + . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" ) func addAnnotationMig(mg *Migrator) { diff --git a/pkg/services/sqlstore/migrations/external_alertmanagers.go b/pkg/services/sqlstore/migrations/external_alertmanagers.go index 781341f7b3f..3403dc50c3e 100644 --- a/pkg/services/sqlstore/migrations/external_alertmanagers.go +++ b/pkg/services/sqlstore/migrations/external_alertmanagers.go @@ -5,12 +5,13 @@ import ( "net/url" "time" + "xorm.io/xorm" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/sqlstore/migrations/ualert" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/util" - "xorm.io/xorm" ) func AddExternalAlertmanagerToDatasourceMigration(mg *migrator.Migrator) { diff --git a/pkg/services/sqlstore/migrations/folder_mig.go b/pkg/services/sqlstore/migrations/folder_mig.go index 4ef08d198e8..faf74c64ed0 100644 --- a/pkg/services/sqlstore/migrations/folder_mig.go +++ b/pkg/services/sqlstore/migrations/folder_mig.go @@ -4,7 +4,6 @@ import ( "github.com/grafana/grafana/pkg/services/sqlstore/migrator" ) -// nolint:unused // this is temporarily unused during feature development func addFolderMigrations(mg *migrator.Migrator) { mg.AddMigration("create folder table", migrator.NewAddTableMigration(folderv1())) @@ -23,8 +22,8 @@ func addFolderMigrations(mg *migrator.Migrator) { })) } -// nolint:unused // this is temporarily unused during feature development func folderv1() migrator.Table { + // Do not make any changes to this schema; introduce new migrations for further changes return migrator.Table{ Name: "folder", Columns: []*migrator.Column{ diff --git a/pkg/services/sqlstore/migrations/migrations.go b/pkg/services/sqlstore/migrations/migrations.go index 30b08f96067..6084dacba03 100644 --- a/pkg/services/sqlstore/migrations/migrations.go +++ b/pkg/services/sqlstore/migrations/migrations.go @@ -107,12 +107,7 @@ func (*OSSMigrations) AddMigration(mg *Migrator) { AddExternalAlertmanagerToDatasourceMigration(mg) - // TODO: This migration will be enabled later in the nested folder feature - // implementation process. It is on hold so we can continue working on the - // store implementation without impacting any grafana instances built off - // main. - // - // addFolderMigrations(mg) + addFolderMigrations(mg) } func addMigrationLogMigrations(mg *Migrator) { diff --git a/pkg/services/sqlstore/migrations/temp_user.go b/pkg/services/sqlstore/migrations/temp_user.go index a4fd1072aa7..af60817f035 100644 --- a/pkg/services/sqlstore/migrations/temp_user.go +++ b/pkg/services/sqlstore/migrations/temp_user.go @@ -3,8 +3,9 @@ package migrations import ( "time" - . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "xorm.io/xorm" + + . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" ) func addTempUserMigrations(mg *Migrator) { diff --git a/pkg/services/sqlstore/migrations/ualert/alert_rule_test.go b/pkg/services/sqlstore/migrations/ualert/alert_rule_test.go index 5e589993cf1..64896d24666 100644 --- a/pkg/services/sqlstore/migrations/ualert/alert_rule_test.go +++ b/pkg/services/sqlstore/migrations/ualert/alert_rule_test.go @@ -5,9 +5,9 @@ import ( "strings" "testing" - "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/components/simplejson" ) func TestMigrateAlertRuleQueries(t *testing.T) { diff --git a/pkg/services/sqlstore/migrations/ualert/tables.go b/pkg/services/sqlstore/migrations/ualert/tables.go index de7953e7747..6598f094149 100644 --- a/pkg/services/sqlstore/migrations/ualert/tables.go +++ b/pkg/services/sqlstore/migrations/ualert/tables.go @@ -3,6 +3,8 @@ package ualert import ( "fmt" + "xorm.io/xorm" + "github.com/grafana/grafana/pkg/services/sqlstore/migrator" ) @@ -12,34 +14,45 @@ const DefaultFieldMaxLength = 190 // UIDMaxLength is the standard size for fields that contain UIDs. const UIDMaxLength = 40 -// AddMigration defines database migrations. +// AddTablesMigrations defines database migrations that affect Grafana Alerting tables. func AddTablesMigrations(mg *migrator.Migrator) { - AddAlertDefinitionMigrations(mg, 60) - AddAlertDefinitionVersionMigrations(mg) - // Create alert_instance table - AlertInstanceMigration(mg) - - // Create alert_rule - AddAlertRuleMigrations(mg, 60) - AddAlertRuleVersionMigrations(mg) - - // Create Alertmanager configurations - AddAlertmanagerConfigMigrations(mg) - - // Create Admin Configuration - AddAlertAdminConfigMigrations(mg) - - // Create provisioning data table - AddProvisioningMigrations(mg) - - AddAlertImageMigrations(mg) - - AddAlertmanagerConfigHistoryMigrations(mg) - ExtractAlertmanagerConfigurationHistoryMigration(mg) + // Migrations are meant to be immutable, any modifications to table structure + // should come in the form of a new migration appended to the end of AddTablesMigrations + // instead of modifying an existing one. This ensure that tables are modified in a consistent and correct order. + historicalTableMigrations(mg) } -// AddAlertDefinitionMigrations should not be modified. -func AddAlertDefinitionMigrations(mg *migrator.Migrator, defaultIntervalSeconds int64) { +// historicalTableMigrations contains those migrations that existed prior to creating the improved messaging around migration immutability. +func historicalTableMigrations(mg *migrator.Migrator) { + // DO NOT EDIT + addAlertDefinitionMigrations(mg, 60) + addAlertDefinitionVersionMigrations(mg) + // Create alert_instance table + alertInstanceMigration(mg) + + // Create alert_rule + addAlertRuleMigrations(mg, 60) + addAlertRuleVersionMigrations(mg) + + // Create Alertmanager configurations + addAlertmanagerConfigMigrations(mg) + + // Create Admin Configuration + addAlertAdminConfigMigrations(mg) + + // Create provisioning data table + addProvisioningMigrations(mg) + + addAlertImageMigrations(mg) + + addAlertmanagerConfigHistoryMigrations(mg) + + extractAlertmanagerConfigurationHistoryMigration(mg) +} + +// addAlertDefinitionMigrations should not be modified. +func addAlertDefinitionMigrations(mg *migrator.Migrator, defaultIntervalSeconds int64) { + // DO NOT EDIT mg.AddMigration("delete alert_definition table", migrator.NewDropTableMigration("alert_definition")) alertDefinition := migrator.Table{ @@ -87,8 +100,9 @@ func AddAlertDefinitionMigrations(mg *migrator.Migrator, defaultIntervalSeconds mg.AddMigration("drop alert_definition table", migrator.NewDropTableMigration("alert_definition")) } -// AddAlertDefinitionMigrations should not be modified. -func AddAlertDefinitionVersionMigrations(mg *migrator.Migrator) { +// addAlertDefinitionMigrations should not be modified. +func addAlertDefinitionVersionMigrations(mg *migrator.Migrator) { + // DO NOT EDIT mg.AddMigration("delete alert_definition_version table", migrator.NewDropTableMigration("alert_definition_version")) alertDefinitionVersion := migrator.Table{ @@ -120,7 +134,8 @@ func AddAlertDefinitionVersionMigrations(mg *migrator.Migrator) { mg.AddMigration("drop alert_definition_version table", migrator.NewDropTableMigration("alert_definition_version")) } -func AlertInstanceMigration(mg *migrator.Migrator) { +func alertInstanceMigration(mg *migrator.Migrator) { + // DO NOT EDIT alertInstance := migrator.Table{ Name: "alert_instance", Columns: []*migrator.Column{ @@ -171,7 +186,8 @@ func AlertInstanceMigration(mg *migrator.Migrator) { })) } -func AddAlertRuleMigrations(mg *migrator.Migrator, defaultIntervalSeconds int64) { +func addAlertRuleMigrations(mg *migrator.Migrator, defaultIntervalSeconds int64) { + // DO NOT EDIT alertRule := migrator.Table{ Name: "alert_rule", Columns: []*migrator.Column{ @@ -272,7 +288,8 @@ func AddAlertRuleMigrations(mg *migrator.Migrator, defaultIntervalSeconds int64) )) } -func AddAlertRuleVersionMigrations(mg *migrator.Migrator) { +func addAlertRuleVersionMigrations(mg *migrator.Migrator) { + // DO NOT EDIT alertRuleVersion := migrator.Table{ Name: "alert_rule_version", Columns: []*migrator.Column{ @@ -335,7 +352,8 @@ func AddAlertRuleVersionMigrations(mg *migrator.Migrator) { )) } -func AddAlertmanagerConfigMigrations(mg *migrator.Migrator) { +func addAlertmanagerConfigMigrations(mg *migrator.Migrator) { + // DO NOT EDIT alertConfiguration := migrator.Table{ Name: "alert_configuration", Columns: []*migrator.Column{ @@ -368,7 +386,8 @@ func AddAlertmanagerConfigMigrations(mg *migrator.Migrator) { })) } -func AddAlertmanagerConfigHistoryMigrations(mg *migrator.Migrator) { +func addAlertmanagerConfigHistoryMigrations(mg *migrator.Migrator) { + // DO NOT EDIT alertConfigHistory := migrator.Table{ Name: "alert_configuration_history", Columns: []*migrator.Column{ @@ -388,7 +407,8 @@ func AddAlertmanagerConfigHistoryMigrations(mg *migrator.Migrator) { mg.AddMigration("create_alert_configuration_history_table", migrator.NewAddTableMigration(alertConfigHistory)) } -func AddAlertAdminConfigMigrations(mg *migrator.Migrator) { +func addAlertAdminConfigMigrations(mg *migrator.Migrator) { + // DO NOT EDIT adminConfiguration := migrator.Table{ Name: "ngalert_configuration", Columns: []*migrator.Column{ @@ -411,7 +431,8 @@ func AddAlertAdminConfigMigrations(mg *migrator.Migrator) { })) } -func AddProvisioningMigrations(mg *migrator.Migrator) { +func addProvisioningMigrations(mg *migrator.Migrator) { + // DO NOT EDIT provisioningTable := migrator.Table{ Name: "provenance_type", Columns: []*migrator.Column{ @@ -430,7 +451,8 @@ func AddProvisioningMigrations(mg *migrator.Migrator) { mg.AddMigration("add index to uniquify (record_key, record_type, org_id) columns", migrator.NewAddIndexMigration(provisioningTable, provisioningTable.Indices[0])) } -func AddAlertImageMigrations(mg *migrator.Migrator) { +func addAlertImageMigrations(mg *migrator.Migrator) { + // DO NOT EDIT imageTable := migrator.Table{ Name: "alert_image", Columns: []*migrator.Column{ @@ -453,3 +475,81 @@ func AddAlertImageMigrations(mg *migrator.Migrator) { Postgres("ALTER TABLE alert_image ALTER COLUMN url TYPE VARCHAR(2048);"). Mysql("ALTER TABLE alert_image MODIFY url VARCHAR(2048) NOT NULL;")) } + +func extractAlertmanagerConfigurationHistoryMigration(mg *migrator.Migrator) { + if !mg.Cfg.UnifiedAlerting.IsEnabled() { + return + } + // Since it's not always consistent as to what state the org ID indexes are in, just drop them all and rebuild from scratch. + // This is not expensive since this table is guaranteed to have a small number of rows. + mg.AddMigration("drop non-unique orgID index on alert_configuration", migrator.NewDropIndexMigration(migrator.Table{Name: "alert_configuration"}, &migrator.Index{Cols: []string{"org_id"}})) + mg.AddMigration("drop unique orgID index on alert_configuration if exists", migrator.NewDropIndexMigration(migrator.Table{Name: "alert_configuration"}, &migrator.Index{Type: migrator.UniqueIndex, Cols: []string{"org_id"}})) + mg.AddMigration("extract alertmanager configuration history to separate table", &extractAlertmanagerConfigurationHistory{}) + mg.AddMigration("add unique index on orgID to alert_configuration", migrator.NewAddIndexMigration(migrator.Table{Name: "alert_configuration"}, &migrator.Index{Type: migrator.UniqueIndex, Cols: []string{"org_id"}})) +} + +type extractAlertmanagerConfigurationHistory struct { + migrator.MigrationBase +} + +// extractAMConfigHistoryConfigModel is the model of an alertmanager configuration row, at the time that the extractAlertmanagerConfigurationHistory migration was run. +// This is not to be used outside of the extractAlertmanagerConfigurationHistory migration. +type extractAMConfigHistoryConfigModel struct { + ID int64 `xorm:"pk autoincr 'id'"` + AlertmanagerConfiguration string + ConfigurationHash string + ConfigurationVersion string + CreatedAt int64 `xorm:"created"` + Default bool + OrgID int64 `xorm:"org_id"` +} + +func (c extractAlertmanagerConfigurationHistory) SQL(migrator.Dialect) string { + return codeMigration +} + +func (c extractAlertmanagerConfigurationHistory) Exec(sess *xorm.Session, migrator *migrator.Migrator) error { + // DO NOT EDIT + var orgs []int64 + if err := sess.Table("alert_configuration").Distinct("org_id").Find(&orgs); err != nil { + return fmt.Errorf("failed to retrieve the organizations with alerting configurations: %w", err) + } + + // Clear out the history table, just in case. It should already be empty. + if _, err := sess.Exec("DELETE FROM alert_configuration_history"); err != nil { + return fmt.Errorf("failed to clear the config history table: %w", err) + } + + for _, orgID := range orgs { + var activeConfigID int64 + has, err := sess.SQL(`SELECT MAX(id) FROM alert_configuration WHERE org_id = ?`, orgID).Get(&activeConfigID) + if err != nil { + return fmt.Errorf("failed to query active config ID for org %d: %w", orgID, err) + } + if !has { + return fmt.Errorf("we previously found a config for org, but later it was unexpectedly missing: %d", orgID) + } + + history := make([]extractAMConfigHistoryConfigModel, 0) + err = sess.Table("alert_configuration").Where("org_id = ? AND id < ?", orgID, activeConfigID).Find(&history) + if err != nil { + return fmt.Errorf("failed to query for non-active configs for org %d: %w", orgID, err) + } + + // Set the IDs back to the default, so XORM will ignore the field and auto-assign them. + for i := range history { + history[i].ID = 0 + } + + _, err = sess.Table("alert_configuration_history").InsertMulti(history) + if err != nil { + return fmt.Errorf("failed to insert historical configs for org: %d: %w", orgID, err) + } + + _, err = sess.Exec("DELETE FROM alert_configuration WHERE org_id = ? AND id < ?", orgID, activeConfigID) + if err != nil { + return fmt.Errorf("failed to evict old configurations for org after moving to history table: %d: %w", orgID, err) + } + } + return nil +} diff --git a/pkg/services/sqlstore/migrations/ualert/ualert.go b/pkg/services/sqlstore/migrations/ualert/ualert.go index d2073b9f023..eb319f03787 100644 --- a/pkg/services/sqlstore/migrations/ualert/ualert.go +++ b/pkg/services/sqlstore/migrations/ualert/ualert.go @@ -12,11 +12,10 @@ import ( "strings" "time" + "github.com/grafana/alerting/alerting/notifier/channels" pb "github.com/prometheus/alertmanager/silence/silencepb" "xorm.io/xorm" - "github.com/grafana/alerting/alerting/notifier/channels" - ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/notifier/channels_config" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" @@ -927,80 +926,3 @@ func (s *uidSet) generateUid() (string, error) { return "", errors.New("failed to generate UID") } - -func ExtractAlertmanagerConfigurationHistoryMigration(mg *migrator.Migrator) { - if !mg.Cfg.UnifiedAlerting.IsEnabled() { - return - } - // Since it's not always consistent as to what state the org ID indexes are in, just drop them all and rebuild from scratch. - // This is not expensive since this table is guaranteed to have a small number of rows. - mg.AddMigration("drop non-unique orgID index on alert_configuration", migrator.NewDropIndexMigration(migrator.Table{Name: "alert_configuration"}, &migrator.Index{Cols: []string{"org_id"}})) - mg.AddMigration("drop unique orgID index on alert_configuration if exists", migrator.NewDropIndexMigration(migrator.Table{Name: "alert_configuration"}, &migrator.Index{Type: migrator.UniqueIndex, Cols: []string{"org_id"}})) - mg.AddMigration("extract alertmanager configuration history to separate table", &extractAlertmanagerConfigurationHistory{}) - mg.AddMigration("add unique index on orgID to alert_configuration", migrator.NewAddIndexMigration(migrator.Table{Name: "alert_configuration"}, &migrator.Index{Type: migrator.UniqueIndex, Cols: []string{"org_id"}})) -} - -type extractAlertmanagerConfigurationHistory struct { - migrator.MigrationBase -} - -// extractAMConfigHistoryConfigModel is the model of an alertmanager configuration row, at the time that the extractAlertmanagerConfigurationHistory migration was run. -// This is not to be used outside of the extractAlertmanagerConfigurationHistory migration. -type extractAMConfigHistoryConfigModel struct { - ID int64 `xorm:"pk autoincr 'id'"` - AlertmanagerConfiguration string - ConfigurationHash string - ConfigurationVersion string - CreatedAt int64 `xorm:"created"` - Default bool - OrgID int64 `xorm:"org_id"` -} - -func (c extractAlertmanagerConfigurationHistory) SQL(migrator.Dialect) string { - return codeMigration -} - -func (c extractAlertmanagerConfigurationHistory) Exec(sess *xorm.Session, migrator *migrator.Migrator) error { - var orgs []int64 - if err := sess.Table("alert_configuration").Distinct("org_id").Find(&orgs); err != nil { - return fmt.Errorf("failed to retrieve the organizations with alerting configurations: %w", err) - } - - // Clear out the history table, just in case. It should already be empty. - if _, err := sess.Exec("DELETE FROM alert_configuration_history"); err != nil { - return fmt.Errorf("failed to clear the config history table: %w", err) - } - - for _, orgID := range orgs { - var activeConfigID int64 - has, err := sess.SQL(`SELECT MAX(id) FROM alert_configuration WHERE org_id = ?`, orgID).Get(&activeConfigID) - if err != nil { - return fmt.Errorf("failed to query active config ID for org %d: %w", orgID, err) - } - if !has { - return fmt.Errorf("we previously found a config for org, but later it was unexpectedly missing: %d", orgID) - } - - history := make([]extractAMConfigHistoryConfigModel, 0) - err = sess.Table("alert_configuration").Where("org_id = ? AND id < ?", orgID, activeConfigID).Find(&history) - if err != nil { - return fmt.Errorf("failed to query for non-active configs for org %d: %w", orgID, err) - } - - // Set the IDs back to the default, so XORM will ignore the field and auto-assign them. - for i := range history { - history[i].ID = 0 - } - - _, err = sess.Table("alert_configuration_history").InsertMulti(history) - if err != nil { - return fmt.Errorf("failed to insert historical configs for org: %d: %w", orgID, err) - } - - _, err = sess.Exec("DELETE FROM alert_configuration WHERE org_id = ? AND id < ?", orgID, activeConfigID) - if err != nil { - return fmt.Errorf("failed to evict old configurations for org after moving to history table: %d: %w", orgID, err) - } - } - return nil -} diff --git a/pkg/services/sqlstore/migrations/user_mig.go b/pkg/services/sqlstore/migrations/user_mig.go index 6c33849621c..6edf5161f47 100644 --- a/pkg/services/sqlstore/migrations/user_mig.go +++ b/pkg/services/sqlstore/migrations/user_mig.go @@ -3,9 +3,10 @@ package migrations import ( "fmt" + "xorm.io/xorm" + . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/util" - "xorm.io/xorm" ) func addUserMigrations(mg *Migrator) { diff --git a/pkg/services/sqlstore/migrator/postgres_dialect.go b/pkg/services/sqlstore/migrator/postgres_dialect.go index b29577e35f6..b996eabac75 100644 --- a/pkg/services/sqlstore/migrator/postgres_dialect.go +++ b/pkg/services/sqlstore/migrator/postgres_dialect.go @@ -9,7 +9,6 @@ import ( "github.com/golang-migrate/migrate/v4/database" "github.com/lib/pq" - "xorm.io/xorm" ) diff --git a/pkg/services/sqlstore/session.go b/pkg/services/sqlstore/session.go index cffd88231a0..062d166963f 100644 --- a/pkg/services/sqlstore/session.go +++ b/pkg/services/sqlstore/session.go @@ -7,11 +7,10 @@ import ( "reflect" "time" + "github.com/mattn/go-sqlite3" "go.opentelemetry.io/otel/attribute" "xorm.io/xorm" - "github.com/mattn/go-sqlite3" - "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" diff --git a/pkg/services/sqlstore/sqlstore_test.go b/pkg/services/sqlstore/sqlstore_test.go index d17ba44c9a0..b85c7dd77b0 100644 --- a/pkg/services/sqlstore/sqlstore_test.go +++ b/pkg/services/sqlstore/sqlstore_test.go @@ -5,8 +5,9 @@ import ( "net/url" "testing" - "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/setting" ) type sqlStoreTest struct { diff --git a/pkg/services/star/api/api.go b/pkg/services/star/api/api.go index 1867e952116..ec231596e4c 100644 --- a/pkg/services/star/api/api.go +++ b/pkg/services/star/api/api.go @@ -6,7 +6,7 @@ import ( "strconv" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/star" "github.com/grafana/grafana/pkg/web" @@ -45,7 +45,7 @@ func (api *API) getDashboardHelper(ctx context.Context, orgID int64, id int64, u return result, nil } -func (api *API) GetStars(c *models.ReqContext) response.Response { +func (api *API) GetStars(c *contextmodel.ReqContext) response.Response { query := star.GetUserStarsQuery{ UserID: c.SignedInUser.UserID, } @@ -85,7 +85,7 @@ func (api *API) GetStars(c *models.ReqContext) response.Response { // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (api *API) StarDashboard(c *models.ReqContext) response.Response { +func (api *API) StarDashboard(c *contextmodel.ReqContext) response.Response { id, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "Invalid dashboard ID", nil) @@ -115,7 +115,7 @@ func (api *API) StarDashboard(c *models.ReqContext) response.Response { // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (api *API) StarDashboardByUID(c *models.ReqContext) response.Response { +func (api *API) StarDashboardByUID(c *contextmodel.ReqContext) response.Response { uid := web.Params(c.Req)[":uid"] if uid == "" { return response.Error(http.StatusBadRequest, "Invalid dashboard UID", nil) @@ -151,7 +151,7 @@ func (api *API) StarDashboardByUID(c *models.ReqContext) response.Response { // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (api *API) UnstarDashboard(c *models.ReqContext) response.Response { +func (api *API) UnstarDashboard(c *contextmodel.ReqContext) response.Response { id, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "Invalid dashboard ID", nil) @@ -181,7 +181,7 @@ func (api *API) UnstarDashboard(c *models.ReqContext) response.Response { // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (api *API) UnstarDashboardByUID(c *models.ReqContext) response.Response { +func (api *API) UnstarDashboardByUID(c *contextmodel.ReqContext) response.Response { uid := web.Params(c.Req)[":uid"] if uid == "" { return response.Error(http.StatusBadRequest, "Invalid dashboard UID", nil) diff --git a/pkg/services/store/entity/entity.pb.go b/pkg/services/store/entity/entity.pb.go index fcf78fd5e8d..eb8f1fe35ec 100644 --- a/pkg/services/store/entity/entity.pb.go +++ b/pkg/services/store/entity/entity.pb.go @@ -7,10 +7,11 @@ package entity import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" + + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" ) const ( diff --git a/pkg/services/store/entity/entity_grpc.pb.go b/pkg/services/store/entity/entity_grpc.pb.go index eb26acf9381..727d3b24ceb 100644 --- a/pkg/services/store/entity/entity_grpc.pb.go +++ b/pkg/services/store/entity/entity_grpc.pb.go @@ -8,6 +8,7 @@ package entity import ( context "context" + grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" diff --git a/pkg/services/store/entity/httpentitystore/service.go b/pkg/services/store/entity/httpentitystore/service.go index 786c8a28bfd..4fe1daee5e9 100644 --- a/pkg/services/store/entity/httpentitystore/service.go +++ b/pkg/services/store/entity/httpentitystore/service.go @@ -8,16 +8,15 @@ import ( "strconv" "strings" + "github.com/grafana/grafana/pkg/api/response" + "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/middleware" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/store/entity" "github.com/grafana/grafana/pkg/services/store/kind" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/web" - - "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/api/routing" - "github.com/grafana/grafana/pkg/models" ) type HTTPEntityStore interface { @@ -60,7 +59,7 @@ func (s *httpEntityStore) RegisterHTTPRoutes(route routing.RouteRegister) { // This function will extract UID+Kind from the requested path "*" in our router // This is far from ideal! but is at least consistent for these endpoints. // This will quickly be revisited as we explore how to encode UID+Kind in a "GRN" format -func (s *httpEntityStore) getGRNFromRequest(c *models.ReqContext) (*entity.GRN, map[string]string, error) { +func (s *httpEntityStore) getGRNFromRequest(c *contextmodel.ReqContext) (*entity.GRN, map[string]string, error) { params := web.Params(c.Req) // Read parameters that are encoded in the URL vals := c.Req.URL.Query() @@ -76,7 +75,7 @@ func (s *httpEntityStore) getGRNFromRequest(c *models.ReqContext) (*entity.GRN, }, params, nil } -func (s *httpEntityStore) doGetEntity(c *models.ReqContext) response.Response { +func (s *httpEntityStore) doGetEntity(c *contextmodel.ReqContext) response.Response { grn, params, err := s.getGRNFromRequest(c) if err != nil { return response.Error(400, err.Error(), err) @@ -111,7 +110,7 @@ func (s *httpEntityStore) doGetEntity(c *models.ReqContext) response.Response { return response.JSON(200, rsp) } -func (s *httpEntityStore) doGetRawEntity(c *models.ReqContext) response.Response { +func (s *httpEntityStore) doGetRawEntity(c *contextmodel.ReqContext) response.Response { grn, params, err := s.getGRNFromRequest(c) if err != nil { return response.Error(400, err.Error(), err) @@ -161,7 +160,7 @@ func (s *httpEntityStore) doGetRawEntity(c *models.ReqContext) response.Response const MAX_UPLOAD_SIZE = 5 * 1024 * 1024 // 5MB -func (s *httpEntityStore) doWriteEntity(c *models.ReqContext) response.Response { +func (s *httpEntityStore) doWriteEntity(c *contextmodel.ReqContext) response.Response { grn, params, err := s.getGRNFromRequest(c) if err != nil { return response.Error(400, err.Error(), err) @@ -187,7 +186,7 @@ func (s *httpEntityStore) doWriteEntity(c *models.ReqContext) response.Response return response.JSON(200, rsp) } -func (s *httpEntityStore) doDeleteEntity(c *models.ReqContext) response.Response { +func (s *httpEntityStore) doDeleteEntity(c *contextmodel.ReqContext) response.Response { grn, params, err := s.getGRNFromRequest(c) if err != nil { return response.Error(400, err.Error(), err) @@ -202,7 +201,7 @@ func (s *httpEntityStore) doDeleteEntity(c *models.ReqContext) response.Response return response.JSON(200, rsp) } -func (s *httpEntityStore) doGetHistory(c *models.ReqContext) response.Response { +func (s *httpEntityStore) doGetHistory(c *contextmodel.ReqContext) response.Response { grn, params, err := s.getGRNFromRequest(c) if err != nil { return response.Error(400, err.Error(), err) @@ -219,7 +218,7 @@ func (s *httpEntityStore) doGetHistory(c *models.ReqContext) response.Response { return response.JSON(200, rsp) } -func (s *httpEntityStore) doUpload(c *models.ReqContext) response.Response { +func (s *httpEntityStore) doUpload(c *contextmodel.ReqContext) response.Response { c.Req.Body = http.MaxBytesReader(c.Resp, c.Req.Body, MAX_UPLOAD_SIZE) if err := c.Req.ParseMultipartForm(MAX_UPLOAD_SIZE); err != nil { msg := fmt.Sprintf("Please limit file uploaded under %s", util.ByteCountSI(MAX_UPLOAD_SIZE)) @@ -302,11 +301,11 @@ func (s *httpEntityStore) doUpload(c *models.ReqContext) response.Response { return response.JSON(200, rsp) } -func (s *httpEntityStore) doListFolder(c *models.ReqContext) response.Response { +func (s *httpEntityStore) doListFolder(c *contextmodel.ReqContext) response.Response { return response.JSON(501, "Not implemented yet") } -func (s *httpEntityStore) doSearch(c *models.ReqContext) response.Response { +func (s *httpEntityStore) doSearch(c *contextmodel.ReqContext) response.Response { vals := c.Req.URL.Query() req := &entity.EntitySearchRequest{ diff --git a/pkg/services/store/entity/tests/common.go b/pkg/services/store/entity/tests/common.go index b40122f9cef..f481365ddf5 100644 --- a/pkg/services/store/entity/tests/common.go +++ b/pkg/services/store/entity/tests/common.go @@ -4,6 +4,10 @@ import ( "context" "testing" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + apikeygenprefix "github.com/grafana/grafana/pkg/components/apikeygenprefixed" "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/server" @@ -14,9 +18,6 @@ import ( "github.com/grafana/grafana/pkg/services/store/entity" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/tests/testinfra" - "github.com/stretchr/testify/require" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" ) func createServiceAccountAdminToken(t *testing.T, env *server.TestEnv) (string, *user.SignedInUser) { diff --git a/pkg/services/store/entity_events.go b/pkg/services/store/entity_events.go index 027900bd9a3..fe4ec4ff3c6 100644 --- a/pkg/services/store/entity_events.go +++ b/pkg/services/store/entity_events.go @@ -148,6 +148,10 @@ func (e *entityEventService) Run(ctx context.Context) error { type dummyEntityEventsService struct { } +func NewDummyEntityEventsService() EntityEventsService { + return dummyEntityEventsService{} +} + func (d dummyEntityEventsService) Run(ctx context.Context) error { return nil } diff --git a/pkg/services/store/http.go b/pkg/services/store/http.go index 47219d2652c..52de0d15a95 100644 --- a/pkg/services/store/http.go +++ b/pkg/services/store/http.go @@ -11,7 +11,7 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/web" ) @@ -57,7 +57,7 @@ func (s *standardStorageService) RegisterHTTPRoutes(storageRoute routing.RouteRe storageRoute.Get("/config", reqGrafanaAdmin, routing.Wrap(s.getConfig)) } -func (s *standardStorageService) doWrite(c *models.ReqContext) response.Response { +func (s *standardStorageService) doWrite(c *contextmodel.ReqContext) response.Response { scope, path := getPathAndScope(c) cmd := &WriteValueRequest{} if err := web.Bind(c.Req, cmd); err != nil { @@ -71,7 +71,7 @@ func (s *standardStorageService) doWrite(c *models.ReqContext) response.Response return response.JSON(200, rsp) } -func (s *standardStorageService) doUpload(c *models.ReqContext) response.Response { +func (s *standardStorageService) doUpload(c *contextmodel.ReqContext) response.Response { type rspInfo struct { Message string `json:"message,omitempty"` Path string `json:"path,omitempty"` @@ -158,7 +158,7 @@ func getMultipartFormValue(req *http.Request, key string) string { return v[0] } -func (s *standardStorageService) read(c *models.ReqContext) response.Response { +func (s *standardStorageService) read(c *contextmodel.ReqContext) response.Response { // full path is api/storage/read/upload/example.jpg, but we only want the part after read scope, path := getPathAndScope(c) file, err := s.Read(c.Req.Context(), c.SignedInUser, scope+"/"+path) @@ -177,7 +177,7 @@ func (s *standardStorageService) read(c *models.ReqContext) response.Response { return response.Respond(200, file.Contents) } -func (s *standardStorageService) getOptions(c *models.ReqContext) response.Response { +func (s *standardStorageService) getOptions(c *contextmodel.ReqContext) response.Response { scope, path := getPathAndScope(c) opts, err := s.getWorkflowOptions(c.Req.Context(), c.SignedInUser, scope+"/"+path) if err != nil { @@ -186,7 +186,7 @@ func (s *standardStorageService) getOptions(c *models.ReqContext) response.Respo return response.JSON(200, opts) } -func (s *standardStorageService) doDelete(c *models.ReqContext) response.Response { +func (s *standardStorageService) doDelete(c *contextmodel.ReqContext) response.Response { // full path is api/storage/delete/upload/example.jpg, but we only want the part after upload scope, path := getPathAndScope(c) @@ -201,7 +201,7 @@ func (s *standardStorageService) doDelete(c *models.ReqContext) response.Respons }) } -func (s *standardStorageService) doDeleteFolder(c *models.ReqContext) response.Response { +func (s *standardStorageService) doDeleteFolder(c *contextmodel.ReqContext) response.Response { body, err := io.ReadAll(c.Req.Body) if err != nil { return response.Error(500, "error reading bytes", err) @@ -230,7 +230,7 @@ func (s *standardStorageService) doDeleteFolder(c *models.ReqContext) response.R }) } -func (s *standardStorageService) doCreateFolder(c *models.ReqContext) response.Response { +func (s *standardStorageService) doCreateFolder(c *contextmodel.ReqContext) response.Response { body, err := io.ReadAll(c.Req.Body) if err != nil { return response.Error(500, "error reading bytes", err) @@ -257,7 +257,7 @@ func (s *standardStorageService) doCreateFolder(c *models.ReqContext) response.R }) } -func (s *standardStorageService) list(c *models.ReqContext) response.Response { +func (s *standardStorageService) list(c *contextmodel.ReqContext) response.Response { params := web.Params(c.Req) path := params["*"] frame, err := s.List(c.Req.Context(), c.SignedInUser, path) @@ -270,7 +270,7 @@ func (s *standardStorageService) list(c *models.ReqContext) response.Response { return response.JSONStreaming(http.StatusOK, frame) } -func (s *standardStorageService) getConfig(c *models.ReqContext) response.Response { +func (s *standardStorageService) getConfig(c *contextmodel.ReqContext) response.Response { roots := make([]RootStorageMeta, 0) orgId := c.OrgID t := s.tree diff --git a/pkg/services/store/k8saccess/client.go b/pkg/services/store/k8saccess/client.go index ecdefb90191..9ae67fbcb0c 100644 --- a/pkg/services/store/k8saccess/client.go +++ b/pkg/services/store/k8saccess/client.go @@ -4,11 +4,12 @@ import ( "net/http" "net/url" - "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/web" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" + + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" + "github.com/grafana/grafana/pkg/web" ) type clientWrapper struct { @@ -84,7 +85,7 @@ func defaultServerUrlFor(config *rest.Config) (*url.URL, string, error) { return rest.DefaultServerURL(host, config.APIPath, schema.GroupVersion{}, defaultTLS) } -func (s *clientWrapper) doProxy(c *models.ReqContext) { +func (s *clientWrapper) doProxy(c *contextmodel.ReqContext) { if s.baseURL == nil { c.Resp.WriteHeader(500) return diff --git a/pkg/services/store/k8saccess/http.go b/pkg/services/store/k8saccess/http.go index 503bc9e4f9d..4a4f64c7210 100644 --- a/pkg/services/store/k8saccess/http.go +++ b/pkg/services/store/k8saccess/http.go @@ -4,7 +4,7 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" ) type httpHelper struct { @@ -25,7 +25,7 @@ func newHTTPHelper(access *k8sAccess, router routing.RouteRegister) *httpHelper return s } -func (s *httpHelper) showClientInfo(c *models.ReqContext) response.Response { +func (s *httpHelper) showClientInfo(c *contextmodel.ReqContext) response.Response { if s.access.sys != nil { info := s.access.sys.getInfo() if s.access.sys.err != nil { @@ -38,7 +38,7 @@ func (s *httpHelper) showClientInfo(c *models.ReqContext) response.Response { }) } -func (s *httpHelper) doProxy(c *models.ReqContext) { +func (s *httpHelper) doProxy(c *contextmodel.ReqContext) { // TODO... this does not yet do a real proxy if s.access.sys != nil { if s.access.sys.err == nil { diff --git a/pkg/services/store/k8saccess/service.go b/pkg/services/store/k8saccess/service.go index fa52768e9f7..ce57f19ef01 100644 --- a/pkg/services/store/k8saccess/service.go +++ b/pkg/services/store/k8saccess/service.go @@ -4,12 +4,13 @@ import ( "os" "path/filepath" - "github.com/grafana/grafana/pkg/api/routing" - "github.com/grafana/grafana/pkg/registry" - "github.com/grafana/grafana/pkg/services/featuremgmt" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" + + "github.com/grafana/grafana/pkg/api/routing" + "github.com/grafana/grafana/pkg/registry" + "github.com/grafana/grafana/pkg/services/featuremgmt" ) type K8SAccess interface { diff --git a/pkg/services/store/kind/playlist/summary_test.go b/pkg/services/store/kind/playlist/summary_test.go index b4d5d726c88..d69cdc5844e 100644 --- a/pkg/services/store/kind/playlist/summary_test.go +++ b/pkg/services/store/kind/playlist/summary_test.go @@ -5,8 +5,9 @@ import ( "encoding/json" "testing" - "github.com/grafana/grafana/pkg/kinds/playlist" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/kinds/playlist" ) func TestPlaylistSummary(t *testing.T) { diff --git a/pkg/services/store/storage_disk.go b/pkg/services/store/storage_disk.go index c5baa7824a2..a02c100fbeb 100644 --- a/pkg/services/store/storage_disk.go +++ b/pkg/services/store/storage_disk.go @@ -5,8 +5,9 @@ import ( "strings" "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/grafana/grafana/pkg/infra/filestorage" "gocloud.dev/blob" + + "github.com/grafana/grafana/pkg/infra/filestorage" ) const rootStorageTypeDisk = "disk" diff --git a/pkg/services/store/storage_git.go b/pkg/services/store/storage_git.go index c3f8f92b173..d14bd515d02 100644 --- a/pkg/services/store/storage_git.go +++ b/pkg/services/store/storage_git.go @@ -11,12 +11,12 @@ import ( "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing/object" - "github.com/grafana/grafana-plugin-sdk-go/data" + "gocloud.dev/blob" + "github.com/grafana/grafana/pkg/infra/filestorage" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" - "gocloud.dev/blob" ) const rootStorageTypeGit = "git" diff --git a/pkg/services/store/storage_sql.go b/pkg/services/store/storage_sql.go index f69f1666c81..57a826f2ecd 100644 --- a/pkg/services/store/storage_sql.go +++ b/pkg/services/store/storage_sql.go @@ -5,10 +5,10 @@ import ( "fmt" "strings" + "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/filestorage" - - "github.com/grafana/grafana-plugin-sdk-go/data" ) const rootStorageTypeSQL = "sql" diff --git a/pkg/services/store/system_users_mock.go b/pkg/services/store/system_users_mock.go index 4a66c62663b..525a807dd69 100644 --- a/pkg/services/store/system_users_mock.go +++ b/pkg/services/store/system_users_mock.go @@ -3,9 +3,9 @@ package store import ( - filestorage "github.com/grafana/grafana/pkg/infra/filestorage" mock "github.com/stretchr/testify/mock" + filestorage "github.com/grafana/grafana/pkg/infra/filestorage" user "github.com/grafana/grafana/pkg/services/user" ) diff --git a/pkg/services/store/system_users_test.go b/pkg/services/store/system_users_test.go index 422d975974f..0e34435cded 100644 --- a/pkg/services/store/system_users_test.go +++ b/pkg/services/store/system_users_test.go @@ -3,9 +3,10 @@ package store import ( "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/filestorage" "github.com/grafana/grafana/pkg/services/user" - "github.com/stretchr/testify/require" ) const admin SystemUserType = "storageAdmin" diff --git a/pkg/services/store/tree.go b/pkg/services/store/tree.go index 49c8b396c26..1d05ee572b2 100644 --- a/pkg/services/store/tree.go +++ b/pkg/services/store/tree.go @@ -6,6 +6,7 @@ import ( "sync" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/infra/filestorage" ac "github.com/grafana/grafana/pkg/services/accesscontrol" ) diff --git a/pkg/services/store/types.go b/pkg/services/store/types.go index 0a8a1cafc81..861ddfd5423 100644 --- a/pkg/services/store/types.go +++ b/pkg/services/store/types.go @@ -5,6 +5,7 @@ import ( "encoding/json" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/infra/filestorage" "github.com/grafana/grafana/pkg/services/user" ) diff --git a/pkg/services/store/utils.go b/pkg/services/store/utils.go index fe275b51291..6537af8ab7b 100644 --- a/pkg/services/store/utils.go +++ b/pkg/services/store/utils.go @@ -3,7 +3,7 @@ package store import ( "strings" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/web" ) @@ -32,7 +32,7 @@ func splitFirstSegment(path string) (string, string) { return path, "" } -func getPathAndScope(c *models.ReqContext) (string, string) { +func getPathAndScope(c *contextmodel.ReqContext) (string, string) { params := web.Params(c.Req) path := params["*"] if path == "" { diff --git a/pkg/services/supportbundles/supportbundlesimpl/api.go b/pkg/services/supportbundles/supportbundlesimpl/api.go index 373789d1a31..74551db8419 100644 --- a/pkg/services/supportbundles/supportbundlesimpl/api.go +++ b/pkg/services/supportbundles/supportbundlesimpl/api.go @@ -10,9 +10,9 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models/roletype" ac "github.com/grafana/grafana/pkg/services/accesscontrol" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/supportbundles" "github.com/grafana/grafana/pkg/web" ) @@ -49,7 +49,7 @@ func (s *Service) registerAPIEndpoints(httpServer *grafanaApi.HTTPServer, routeR }) } -func (s *Service) handleList(ctx *models.ReqContext) response.Response { +func (s *Service) handleList(ctx *contextmodel.ReqContext) response.Response { bundles, err := s.list(ctx.Req.Context()) if err != nil { return response.Error(http.StatusInternalServerError, "failed to list bundles", err) @@ -63,7 +63,7 @@ func (s *Service) handleList(ctx *models.ReqContext) response.Response { return response.JSON(http.StatusOK, data) } -func (s *Service) handleCreate(ctx *models.ReqContext) response.Response { +func (s *Service) handleCreate(ctx *contextmodel.ReqContext) response.Response { type command struct { Collectors []string `json:"collectors"` } @@ -86,7 +86,7 @@ func (s *Service) handleCreate(ctx *models.ReqContext) response.Response { return response.JSON(http.StatusCreated, data) } -func (s *Service) handleDownload(ctx *models.ReqContext) response.Response { +func (s *Service) handleDownload(ctx *contextmodel.ReqContext) response.Response { uid := web.Params(ctx.Req)[":uid"] bundle, err := s.get(ctx.Req.Context(), uid) if err != nil { @@ -102,7 +102,7 @@ func (s *Service) handleDownload(ctx *models.ReqContext) response.Response { return response.CreateNormalResponse(ctx.Resp.Header(), bundle.TarBytes, http.StatusOK) } -func (s *Service) handleRemove(ctx *models.ReqContext) response.Response { +func (s *Service) handleRemove(ctx *contextmodel.ReqContext) response.Response { uid := web.Params(ctx.Req)[":uid"] err := s.remove(ctx.Req.Context(), uid) if err != nil { @@ -112,7 +112,7 @@ func (s *Service) handleRemove(ctx *models.ReqContext) response.Response { return response.Respond(http.StatusOK, "successfully removed the support bundle") } -func (s *Service) handleGetCollectors(ctx *models.ReqContext) response.Response { +func (s *Service) handleGetCollectors(ctx *contextmodel.ReqContext) response.Response { collectors := make([]supportbundles.Collector, 0, len(s.collectors)) for _, c := range s.collectors { diff --git a/pkg/services/supportbundles/supportbundlesimpl/service_test.go b/pkg/services/supportbundles/supportbundlesimpl/service_test.go index a2c3176b15e..39241804020 100644 --- a/pkg/services/supportbundles/supportbundlesimpl/service_test.go +++ b/pkg/services/supportbundles/supportbundlesimpl/service_test.go @@ -4,10 +4,11 @@ import ( "context" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/supportbundles" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/require" ) func TestService_RegisterSupportItemCollector(t *testing.T) { diff --git a/pkg/services/supportbundles/supportbundlesimpl/store.go b/pkg/services/supportbundles/supportbundlesimpl/store.go index ca57a072ece..921ee551b30 100644 --- a/pkg/services/supportbundles/supportbundlesimpl/store.go +++ b/pkg/services/supportbundles/supportbundlesimpl/store.go @@ -9,6 +9,7 @@ import ( "time" "github.com/google/uuid" + "github.com/grafana/grafana/pkg/infra/kvstore" "github.com/grafana/grafana/pkg/services/supportbundles" "github.com/grafana/grafana/pkg/services/user" diff --git a/pkg/services/tag/tagimpl/store_test.go b/pkg/services/tag/tagimpl/store_test.go index 0b865587ef5..3d631136d39 100644 --- a/pkg/services/tag/tagimpl/store_test.go +++ b/pkg/services/tag/tagimpl/store_test.go @@ -4,10 +4,10 @@ import ( "context" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/services/tag" - - "github.com/stretchr/testify/require" ) type getStore func(db.DB) store diff --git a/pkg/services/teamguardian/database/database_mock.go b/pkg/services/teamguardian/database/database_mock.go index 3b58cc06ebb..0a7dd142e56 100644 --- a/pkg/services/teamguardian/database/database_mock.go +++ b/pkg/services/teamguardian/database/database_mock.go @@ -3,8 +3,9 @@ package database import ( "context" - "github.com/grafana/grafana/pkg/services/team" "github.com/stretchr/testify/mock" + + "github.com/grafana/grafana/pkg/services/team" ) type TeamGuardianStoreMock struct { diff --git a/pkg/services/teamguardian/manager/service_mock.go b/pkg/services/teamguardian/manager/service_mock.go index 8ba08d98c45..b561b926d69 100644 --- a/pkg/services/teamguardian/manager/service_mock.go +++ b/pkg/services/teamguardian/manager/service_mock.go @@ -3,8 +3,9 @@ package manager import ( "context" - "github.com/grafana/grafana/pkg/services/user" "github.com/stretchr/testify/mock" + + "github.com/grafana/grafana/pkg/services/user" ) type TeamGuardianMock struct { diff --git a/pkg/services/thumbs/crawler.go b/pkg/services/thumbs/crawler.go index 86647b9df0c..cca086cf51d 100644 --- a/pkg/services/thumbs/crawler.go +++ b/pkg/services/thumbs/crawler.go @@ -10,7 +10,6 @@ import ( "sync" "time" - "github.com/grafana/grafana/pkg/setting" "golang.org/x/sync/errgroup" "github.com/grafana/grafana/pkg/infra/log" @@ -18,6 +17,7 @@ import ( "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/live" "github.com/grafana/grafana/pkg/services/rendering" + "github.com/grafana/grafana/pkg/setting" ) type simpleCrawler struct { diff --git a/pkg/services/thumbs/datasources_lookup.go b/pkg/services/thumbs/datasources_lookup.go index 20af4c19a4b..32ce1d24ec0 100644 --- a/pkg/services/thumbs/datasources_lookup.go +++ b/pkg/services/thumbs/datasources_lookup.go @@ -7,6 +7,7 @@ import ( "fmt" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/searchV2" "github.com/grafana/grafana/pkg/tsdb/grafanads" diff --git a/pkg/services/thumbs/datasources_lookup_test.go b/pkg/services/thumbs/datasources_lookup_test.go index 60484739d47..2643655c59a 100644 --- a/pkg/services/thumbs/datasources_lookup_test.go +++ b/pkg/services/thumbs/datasources_lookup_test.go @@ -7,10 +7,11 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/services/searchV2" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/searchV2" ) var ( diff --git a/pkg/services/thumbs/dummy.go b/pkg/services/thumbs/dummy.go index 09d2045cb5b..9f77fe37891 100644 --- a/pkg/services/thumbs/dummy.go +++ b/pkg/services/thumbs/dummy.go @@ -5,7 +5,7 @@ import ( "net/http" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" ) // When the feature flag is not enabled we just implement a dummy service @@ -15,15 +15,15 @@ func (ds *dummyService) GetUsageStats(ctx context.Context) map[string]interface{ return make(map[string]interface{}) } -func (ds *dummyService) GetImage(c *models.ReqContext) { +func (ds *dummyService) GetImage(c *contextmodel.ReqContext) { c.JSON(400, map[string]string{"error": "invalid size"}) } -func (ds *dummyService) UpdateThumbnailState(c *models.ReqContext) { +func (ds *dummyService) UpdateThumbnailState(c *contextmodel.ReqContext) { c.JSON(400, map[string]string{"error": "invalid size"}) } -func (ds *dummyService) SetImage(c *models.ReqContext) { +func (ds *dummyService) SetImage(c *contextmodel.ReqContext) { c.JSON(400, map[string]string{"error": "invalid size"}) } @@ -31,7 +31,7 @@ func (ds *dummyService) Enabled() bool { return false } -func (ds *dummyService) GetDashboardPreviewsSetupSettings(c *models.ReqContext) dashboardPreviewsSetupConfig { +func (ds *dummyService) GetDashboardPreviewsSetupSettings(c *contextmodel.ReqContext) dashboardPreviewsSetupConfig { return dashboardPreviewsSetupConfig{ SystemRequirements: dashboardPreviewsSystemRequirements{ Met: false, @@ -41,19 +41,19 @@ func (ds *dummyService) GetDashboardPreviewsSetupSettings(c *models.ReqContext) } } -func (ds *dummyService) StartCrawler(c *models.ReqContext) response.Response { +func (ds *dummyService) StartCrawler(c *contextmodel.ReqContext) response.Response { result := make(map[string]string) result["error"] = "Not enabled" return response.JSON(http.StatusOK, result) } -func (ds *dummyService) StopCrawler(c *models.ReqContext) response.Response { +func (ds *dummyService) StopCrawler(c *contextmodel.ReqContext) response.Response { result := make(map[string]string) result["error"] = "Not enabled" return response.JSON(http.StatusOK, result) } -func (ds *dummyService) CrawlerStatus(c *models.ReqContext) response.Response { +func (ds *dummyService) CrawlerStatus(c *contextmodel.ReqContext) response.Response { result := make(map[string]string) result["error"] = "Not enabled" return response.JSON(http.StatusOK, result) diff --git a/pkg/services/thumbs/service.go b/pkg/services/thumbs/service.go index da15dc8a1d2..f34eb8af566 100644 --- a/pkg/services/thumbs/service.go +++ b/pkg/services/thumbs/service.go @@ -16,6 +16,7 @@ import ( "github.com/grafana/grafana/pkg/infra/serverlock" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/registry" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/datasources/permissions" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -32,17 +33,17 @@ type Service interface { registry.ProvidesUsageStats Run(ctx context.Context) error Enabled() bool - GetImage(c *models.ReqContext) - GetDashboardPreviewsSetupSettings(c *models.ReqContext) dashboardPreviewsSetupConfig + GetImage(c *contextmodel.ReqContext) + GetDashboardPreviewsSetupSettings(c *contextmodel.ReqContext) dashboardPreviewsSetupConfig // from dashboard page - SetImage(c *models.ReqContext) // form post - UpdateThumbnailState(c *models.ReqContext) + SetImage(c *contextmodel.ReqContext) // form post + UpdateThumbnailState(c *contextmodel.ReqContext) // Must be admin - StartCrawler(c *models.ReqContext) response.Response - StopCrawler(c *models.ReqContext) response.Response - CrawlerStatus(c *models.ReqContext) response.Response + StartCrawler(c *contextmodel.ReqContext) response.Response + StopCrawler(c *contextmodel.ReqContext) response.Response + CrawlerStatus(c *contextmodel.ReqContext) response.Response } type thumbService struct { @@ -154,7 +155,7 @@ func (hs *thumbService) Enabled() bool { return hs.features.IsEnabled(featuremgmt.FlagDashboardPreviews) } -func (hs *thumbService) parseImageReq(c *models.ReqContext, checkSave bool) *previewRequest { +func (hs *thumbService) parseImageReq(c *contextmodel.ReqContext, checkSave bool) *previewRequest { params := web.Params(c.Req) kind, err := ParseThumbnailKind(params[":kind"]) @@ -199,7 +200,7 @@ type updateThumbnailStateRequest struct { State ThumbnailState `json:"state" binding:"Required"` } -func (hs *thumbService) UpdateThumbnailState(c *models.ReqContext) { +func (hs *thumbService) UpdateThumbnailState(c *contextmodel.ReqContext) { req := hs.parseImageReq(c, false) if req == nil { return // already returned value @@ -231,7 +232,7 @@ func (hs *thumbService) UpdateThumbnailState(c *models.ReqContext) { c.JSON(http.StatusOK, map[string]string{"success": "true"}) } -func (hs *thumbService) GetImage(c *models.ReqContext) { +func (hs *thumbService) GetImage(c *contextmodel.ReqContext) { req := hs.parseImageReq(c, false) if req == nil { return // already returned value @@ -274,7 +275,7 @@ func (hs *thumbService) GetImage(c *models.ReqContext) { } } -func (hs *thumbService) hasAccessToPreview(c *models.ReqContext, res *DashboardThumbnail, req *previewRequest) bool { +func (hs *thumbService) hasAccessToPreview(c *contextmodel.ReqContext, res *DashboardThumbnail, req *previewRequest) bool { if !hs.licensing.FeatureEnabled("accesscontrol.enforcement") { return true } @@ -318,7 +319,7 @@ func (hs *thumbService) hasAccessToPreview(c *models.ReqContext, res *DashboardT return true } -func (hs *thumbService) GetDashboardPreviewsSetupSettings(c *models.ReqContext) dashboardPreviewsSetupConfig { +func (hs *thumbService) GetDashboardPreviewsSetupSettings(c *contextmodel.ReqContext) dashboardPreviewsSetupConfig { return hs.getDashboardPreviewsSetupSettings(c.Req.Context()) } @@ -361,7 +362,7 @@ func (hs *thumbService) getSystemRequirements(ctx context.Context) dashboardPrev } // Hack for now -- lets you upload images explicitly -func (hs *thumbService) SetImage(c *models.ReqContext) { +func (hs *thumbService) SetImage(c *contextmodel.ReqContext) { req := hs.parseImageReq(c, false) if req == nil { return // already returned value @@ -423,7 +424,7 @@ func (hs *thumbService) SetImage(c *models.ReqContext) { c.JSON(http.StatusOK, map[string]int{"OK": len(fileBytes)}) } -func (hs *thumbService) StartCrawler(c *models.ReqContext) response.Response { +func (hs *thumbService) StartCrawler(c *contextmodel.ReqContext) response.Response { body, err := io.ReadAll(c.Req.Body) if err != nil { return response.Error(500, "error reading bytes", err) @@ -451,7 +452,7 @@ func (hs *thumbService) StartCrawler(c *models.ReqContext) response.Response { return response.JSON(http.StatusOK, status) } -func (hs *thumbService) StopCrawler(c *models.ReqContext) response.Response { +func (hs *thumbService) StopCrawler(c *contextmodel.ReqContext) response.Response { msg, err := hs.renderer.Stop() if err != nil { return response.Error(500, "error starting", err) @@ -459,7 +460,7 @@ func (hs *thumbService) StopCrawler(c *models.ReqContext) response.Response { return response.JSON(http.StatusOK, msg) } -func (hs *thumbService) CrawlerStatus(c *models.ReqContext) response.Response { +func (hs *thumbService) CrawlerStatus(c *contextmodel.ReqContext) response.Response { msg, err := hs.renderer.Status() if err != nil { return response.Error(500, "error starting", err) @@ -468,7 +469,7 @@ func (hs *thumbService) CrawlerStatus(c *models.ReqContext) response.Response { } // Ideally this service would not require first looking up the full dashboard just to bet the id! -func (hs *thumbService) getStatus(c *models.ReqContext, uid string, checkSave bool) (int, error) { +func (hs *thumbService) getStatus(c *contextmodel.ReqContext, uid string, checkSave bool) (int, error) { guardian, err := guardian.NewByUID(c.Req.Context(), uid, c.OrgID, c.SignedInUser) if err != nil { return 0, err diff --git a/pkg/services/user/userimpl/user_test.go b/pkg/services/user/userimpl/user_test.go index 53f63e72940..4f3b87d23b4 100644 --- a/pkg/services/user/userimpl/user_test.go +++ b/pkg/services/user/userimpl/user_test.go @@ -5,6 +5,9 @@ import ( "errors" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/models/roletype" "github.com/grafana/grafana/pkg/services/org" @@ -12,9 +15,6 @@ import ( "github.com/grafana/grafana/pkg/services/team/teamtest" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestUserService(t *testing.T) { diff --git a/pkg/setting/date_formats_test.go b/pkg/setting/date_formats_test.go index 36048fd7302..9a4be580d84 100644 --- a/pkg/setting/date_formats_test.go +++ b/pkg/setting/date_formats_test.go @@ -3,10 +3,9 @@ package setting import ( "testing" - "gopkg.in/ini.v1" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "gopkg.in/ini.v1" ) func TestValueAsTimezone(t *testing.T) { diff --git a/pkg/setting/expanders_test.go b/pkg/setting/expanders_test.go index a4687157ddd..fbf2b6bea64 100644 --- a/pkg/setting/expanders_test.go +++ b/pkg/setting/expanders_test.go @@ -7,9 +7,8 @@ import ( "os" "testing" - "github.com/stretchr/testify/require" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestExpandVar_EnvSuccessful(t *testing.T) { diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index e4f6d211b68..4734f5c12c1 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -21,16 +21,15 @@ import ( "strings" "time" + "github.com/gobwas/glob" "github.com/grafana/grafana-aws-sdk/pkg/awsds" "github.com/grafana/grafana-azure-sdk-go/azsettings" "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" + "github.com/prometheus/common/model" + "gopkg.in/ini.v1" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/util" - - "github.com/gobwas/glob" - "github.com/prometheus/common/model" - "gopkg.in/ini.v1" ) type Scheme string @@ -276,6 +275,8 @@ type Cfg struct { PluginAdminEnabled bool PluginAdminExternalManageEnabled bool + PluginsCDNURLTemplate string + // Panels DisableSanitizeHtml bool @@ -499,6 +500,9 @@ type Cfg struct { SecureSocksDSProxy SecureSocksDSProxySettings + // Okta OAuth + OktaSkipOrgRoleSync bool + // Access Control RBACEnabled bool RBACPermissionCache bool @@ -1393,6 +1397,11 @@ func readAuthGitlabSettings(iniFile *ini.File, cfg *Cfg) { cfg.GitLabSkipOrgRoleSync = sec.Key("skip_org_role_sync").MustBool(false) } +func readAuthOktaSettings(iniFile *ini.File, cfg *Cfg) { + sec := iniFile.Section("auth.okta") + cfg.OktaSkipOrgRoleSync = sec.Key("skip_org_role_sync").MustBool(false) +} + func readAuthSettings(iniFile *ini.File, cfg *Cfg) (err error) { auth := iniFile.Section("auth") @@ -1453,6 +1462,9 @@ func readAuthSettings(iniFile *ini.File, cfg *Cfg) (err error) { // GitLab Auth readAuthGitlabSettings(iniFile, cfg) + // Okta Auth + readAuthOktaSettings(iniFile, cfg) + // anonymous access AnonymousEnabled = iniFile.Section("auth.anonymous").Key("enabled").MustBool(false) cfg.AnonymousEnabled = AnonymousEnabled diff --git a/pkg/setting/setting_azure_test.go b/pkg/setting/setting_azure_test.go index b5fe304df49..7df70e823b4 100644 --- a/pkg/setting/setting_azure_test.go +++ b/pkg/setting/setting_azure_test.go @@ -4,7 +4,6 @@ import ( "testing" "github.com/grafana/grafana-azure-sdk-go/azsettings" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/pkg/setting/setting_feature_toggles.go b/pkg/setting/setting_feature_toggles.go index abef83d7c48..a29f24adab3 100644 --- a/pkg/setting/setting_feature_toggles.go +++ b/pkg/setting/setting_feature_toggles.go @@ -3,8 +3,9 @@ package setting import ( "strconv" - "github.com/grafana/grafana/pkg/util" "gopkg.in/ini.v1" + + "github.com/grafana/grafana/pkg/util" ) // @deprecated -- should use `featuremgmt.FeatureToggles` diff --git a/pkg/setting/setting_plugins.go b/pkg/setting/setting_plugins.go index 95d5f0e7fd0..8c29a2e52ff 100644 --- a/pkg/setting/setting_plugins.go +++ b/pkg/setting/setting_plugins.go @@ -26,6 +26,7 @@ func extractPluginSettings(sections []*ini.Section) PluginSettings { func (cfg *Cfg) readPluginSettings(iniFile *ini.File) error { pluginsSection := iniFile.Section("plugins") + cfg.PluginsEnableAlpha = pluginsSection.Key("enable_alpha").MustBool(false) cfg.PluginsAppsSkipVerifyTLS = pluginsSection.Key("app_tls_skip_verify_insecure").MustBool(false) cfg.PluginSettings = extractPluginSettings(iniFile.Sections()) @@ -47,5 +48,8 @@ func (cfg *Cfg) readPluginSettings(iniFile *ini.File) error { cfg.PluginCatalogHiddenPlugins = append(cfg.PluginCatalogHiddenPlugins, plug) } + // Plugins CDN settings + cfg.PluginsCDNURLTemplate = strings.TrimRight(pluginsSection.Key("cdn_base_url").MustString(""), "/") + return nil } diff --git a/pkg/setting/setting_session_test.go b/pkg/setting/setting_session_test.go index 07554d87885..5c86811dbe7 100644 --- a/pkg/setting/setting_session_test.go +++ b/pkg/setting/setting_session_test.go @@ -4,9 +4,9 @@ import ( "path/filepath" "testing" - "github.com/grafana/grafana/pkg/infra/log/logtest" - "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/infra/log/logtest" ) func TestSessionSettings(t *testing.T) { diff --git a/pkg/setting/setting_test.go b/pkg/setting/setting_test.go index e46c8ae10b4..2bc539cbeb4 100644 --- a/pkg/setting/setting_test.go +++ b/pkg/setting/setting_test.go @@ -14,7 +14,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "gopkg.in/ini.v1" ) diff --git a/pkg/setting/setting_unified_alerting.go b/pkg/setting/setting_unified_alerting.go index d81511fa3e4..d417be97417 100644 --- a/pkg/setting/setting_unified_alerting.go +++ b/pkg/setting/setting_unified_alerting.go @@ -8,11 +8,10 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" - - "github.com/grafana/grafana/pkg/util" - "github.com/prometheus/alertmanager/cluster" "gopkg.in/ini.v1" + + "github.com/grafana/grafana/pkg/util" ) const ( diff --git a/pkg/tests/api/alerting/api_alertmanager_configuration_test.go b/pkg/tests/api/alerting/api_alertmanager_configuration_test.go index 59b06869c94..e6eb902872c 100644 --- a/pkg/tests/api/alerting/api_alertmanager_configuration_test.go +++ b/pkg/tests/api/alerting/api_alertmanager_configuration_test.go @@ -10,14 +10,15 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgimpl" "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/tests/testinfra" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestIntegrationAlertmanagerConfigurationIsTransactional(t *testing.T) { diff --git a/pkg/tests/api/alerting/api_provisioning_test.go b/pkg/tests/api/alerting/api_provisioning_test.go index ce115c647ee..4bef914bb2a 100644 --- a/pkg/tests/api/alerting/api_provisioning_test.go +++ b/pkg/tests/api/alerting/api_provisioning_test.go @@ -7,10 +7,11 @@ import ( "net/http" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/tests/testinfra" - "github.com/stretchr/testify/require" ) func TestIntegrationProvisioning(t *testing.T) { diff --git a/pkg/tests/api/azuremonitor/azuremonitor_test.go b/pkg/tests/api/azuremonitor/azuremonitor_test.go index e39184e36e5..c262c7bbce0 100644 --- a/pkg/tests/api/azuremonitor/azuremonitor_test.go +++ b/pkg/tests/api/azuremonitor/azuremonitor_test.go @@ -10,13 +10,14 @@ import ( "net/http/httptest" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/tests/testinfra" - "github.com/stretchr/testify/require" ) func TestIntegrationAzureMonitor(t *testing.T) { diff --git a/pkg/tests/api/correlations/correlations_create_test.go b/pkg/tests/api/correlations/correlations_create_test.go index 182be0c70dc..3921767c6fa 100644 --- a/pkg/tests/api/correlations/correlations_create_test.go +++ b/pkg/tests/api/correlations/correlations_create_test.go @@ -7,11 +7,12 @@ import ( "net/http" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/services/correlations" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" - "github.com/stretchr/testify/require" ) func TestIntegrationCreateCorrelation(t *testing.T) { diff --git a/pkg/tests/api/correlations/correlations_delete_test.go b/pkg/tests/api/correlations/correlations_delete_test.go index defb10e4551..9002a5582d7 100644 --- a/pkg/tests/api/correlations/correlations_delete_test.go +++ b/pkg/tests/api/correlations/correlations_delete_test.go @@ -7,11 +7,12 @@ import ( "net/http" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/services/correlations" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" - "github.com/stretchr/testify/require" ) func TestIntegrationDeleteCorrelation(t *testing.T) { diff --git a/pkg/tests/api/correlations/correlations_update_test.go b/pkg/tests/api/correlations/correlations_update_test.go index 5b603f9eb71..a085abac46f 100644 --- a/pkg/tests/api/correlations/correlations_update_test.go +++ b/pkg/tests/api/correlations/correlations_update_test.go @@ -7,11 +7,12 @@ import ( "net/http" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/services/correlations" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" - "github.com/stretchr/testify/require" ) func TestIntegrationUpdateCorrelation(t *testing.T) { diff --git a/pkg/tests/api/elasticsearch/elasticsearch_test.go b/pkg/tests/api/elasticsearch/elasticsearch_test.go index 9c138f1ac5f..ad6204de604 100644 --- a/pkg/tests/api/elasticsearch/elasticsearch_test.go +++ b/pkg/tests/api/elasticsearch/elasticsearch_test.go @@ -10,13 +10,14 @@ import ( "net/http/httptest" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/tests/testinfra" - "github.com/stretchr/testify/require" ) func TestIntegrationElasticsearch(t *testing.T) { diff --git a/pkg/tests/api/graphite/graphite_test.go b/pkg/tests/api/graphite/graphite_test.go index 0b53ba56305..f70f41802e2 100644 --- a/pkg/tests/api/graphite/graphite_test.go +++ b/pkg/tests/api/graphite/graphite_test.go @@ -10,13 +10,14 @@ import ( "net/http/httptest" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/tests/testinfra" - "github.com/stretchr/testify/require" ) func TestIntegrationGraphite(t *testing.T) { diff --git a/pkg/tests/api/influxdb/influxdb_test.go b/pkg/tests/api/influxdb/influxdb_test.go index a76ba3171e7..70740bb81a9 100644 --- a/pkg/tests/api/influxdb/influxdb_test.go +++ b/pkg/tests/api/influxdb/influxdb_test.go @@ -10,13 +10,14 @@ import ( "net/http/httptest" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/tests/testinfra" - "github.com/stretchr/testify/require" ) func TestIntegrationInflux(t *testing.T) { diff --git a/pkg/tests/api/loki/loki_test.go b/pkg/tests/api/loki/loki_test.go index 9cd826b4def..6e86628ac53 100644 --- a/pkg/tests/api/loki/loki_test.go +++ b/pkg/tests/api/loki/loki_test.go @@ -10,13 +10,14 @@ import ( "net/http/httptest" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/tests/testinfra" - "github.com/stretchr/testify/require" ) func TestIntegrationLoki(t *testing.T) { diff --git a/pkg/tests/api/opentdsb/opentdsb_test.go b/pkg/tests/api/opentdsb/opentdsb_test.go index 12598b02f9d..f464757fa1a 100644 --- a/pkg/tests/api/opentdsb/opentdsb_test.go +++ b/pkg/tests/api/opentdsb/opentdsb_test.go @@ -10,13 +10,14 @@ import ( "net/http/httptest" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/tests/testinfra" - "github.com/stretchr/testify/require" ) func TestIntegrationOpenTSDB(t *testing.T) { diff --git a/pkg/tests/api/plugins/api_plugins_test.go b/pkg/tests/api/plugins/api_plugins_test.go index 251c9389310..1e7be227b39 100644 --- a/pkg/tests/api/plugins/api_plugins_test.go +++ b/pkg/tests/api/plugins/api_plugins_test.go @@ -11,15 +11,15 @@ import ( "path/filepath" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/services/org/orgimpl" "github.com/grafana/grafana/pkg/services/quota/quotaimpl" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/userimpl" "github.com/grafana/grafana/pkg/tests/testinfra" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) const ( diff --git a/pkg/tests/api/plugins/backendplugin/backendplugin_test.go b/pkg/tests/api/plugins/backendplugin/backendplugin_test.go index 9b6f58b60a4..7630033d4ac 100644 --- a/pkg/tests/api/plugins/backendplugin/backendplugin_test.go +++ b/pkg/tests/api/plugins/backendplugin/backendplugin_test.go @@ -12,6 +12,9 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/stretchr/testify/require" + "golang.org/x/oauth2" + "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" @@ -22,8 +25,6 @@ import ( "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/tests/testinfra" - "github.com/stretchr/testify/require" - "golang.org/x/oauth2" ) const loginCookieName = "grafana_session" diff --git a/pkg/tests/api/prometheus/prometheus_test.go b/pkg/tests/api/prometheus/prometheus_test.go index eb5c0dc34f2..c5b9f258c42 100644 --- a/pkg/tests/api/prometheus/prometheus_test.go +++ b/pkg/tests/api/prometheus/prometheus_test.go @@ -10,13 +10,14 @@ import ( "net/http/httptest" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/tests/testinfra" - "github.com/stretchr/testify/require" ) func TestIntegrationPrometheus(t *testing.T) { diff --git a/pkg/tests/web/index_view_test.go b/pkg/tests/web/index_view_test.go index e2ef840ede6..8286abde819 100644 --- a/pkg/tests/web/index_view_test.go +++ b/pkg/tests/web/index_view_test.go @@ -7,9 +7,10 @@ import ( "strings" "testing" - "github.com/grafana/grafana/pkg/tests/testinfra" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/tests/testinfra" ) // TestIntegrationIndexView tests the Grafana index view. diff --git a/pkg/tsdb/cloudwatch/constants/metrics.go b/pkg/tsdb/cloudwatch/constants/metrics.go index c2be49a9d67..39e72be22a3 100644 --- a/pkg/tsdb/cloudwatch/constants/metrics.go +++ b/pkg/tsdb/cloudwatch/constants/metrics.go @@ -27,7 +27,7 @@ var NamespaceMetricsMap = map[string][]string{ "AWS/DDoSProtection": {"DDoSDetected", "DDoSAttackBitsPerSecond", "DDoSAttackPacketsPerSecond", "DDoSAttackRequestsPerSecond", "VolumeBitsPerSecond", "VolumePacketsPerSecond"}, "AWS/DMS": {"CDCChangesDiskSource", "CDCChangesDiskTarget", "CDCChangesMemorySource", "CDCChangesMemoryTarget", "CDCIncomingChanges", "CDCLatencySource", "CDCLatencyTarget", "CDCThroughputBandwidthSource", "CDCThroughputBandwidthTarget", "CDCThroughputRowsSource", "CDCThroughputRowsTarget", "CPUUtilization", "FreeStorageSpace", "FreeableMemory", "FullLoadThroughputBandwidthSource", "FullLoadThroughputBandwidthTarget", "FullLoadThroughputRowsSource", "FullLoadThroughputRowsTarget", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "ReadIOPS", "ReadLatency", "ReadThroughput", "SwapUsage", "WriteIOPS", "WriteLatency", "WriteThroughput"}, "AWS/DocDB": {"BackupRetentionPeriodStorageUsed", "BufferCacheHitRatio", "ChangeStreamLogSize", "CPUUtilization", "DatabaseConnections", "DBInstanceReplicaLag", "DBClusterReplicaLagMaximum", "DBClusterReplicaLagMinimum", "DiskQueueDepth", "EngineUptime", "FreeableMemory", "FreeLocalStorage", "NetworkReceiveThroughput", "NetworkThroughput", "NetworkTransmitThroughput", "ReadIOPS", "ReadLatency", "ReadThroughput", "SnapshotStorageUsed", "SwapUsage", "TotalBackupStorageBilled", "VolumeBytesUsed", "VolumeReadIOPs", "VolumeWriteIOPs", "WriteIOPS", "WriteLatency", "WriteThroughput"}, - "AWS/DX": {"ConnectionBpsEgress", "ConnectionBpsIngress", "ConnectionErrorCount", "ConnectionCRCErrorCount", "ConnectionLightLevelRx", "ConnectionLightLevelTx", "ConnectionPpsEgress", "ConnectionPpsIngress", "ConnectionState", "VirtualInterfaceBpsEgress", "VirtualInterfaceBpsIngress", "VirtualInterfacePpsEgress", "VirtualInterfacePpsIngress"}, + "AWS/DX": {"ConnectionEncryptionState", "ConnectionBpsEgress", "ConnectionBpsIngress", "ConnectionErrorCount", "ConnectionCRCErrorCount", "ConnectionLightLevelRx", "ConnectionLightLevelTx", "ConnectionPpsEgress", "ConnectionPpsIngress", "ConnectionState", "VirtualInterfaceBpsEgress", "VirtualInterfaceBpsIngress", "VirtualInterfacePpsEgress", "VirtualInterfacePpsIngress"}, "AWS/DAX": {"CPUUtilization", "NetworkPacketsIn", "NetworkPacketsOut", "GetItemRequestCount", "BatchGetItemRequestCount", "BatchWriteItemRequestCount", "DeleteItemRequestCount", "PutItemRequestCount", "UpdateItemRequestCount", "TransactWriteItemsCount", "TransactGetItemsCount", "ItemCacheHits", "ItemCacheMisses", "QueryCacheHits", "QueryCacheMisses", "ScanCacheHits", "ScanCacheMisses", "TotalRequestCount", "ErrorRequestCount", "FaultRequestCount", "FailedRequestCount", "QueryRequestCount", "ScanRequestCount", "ClientConnections", "EstimatedDbSize", "EvictedSize"}, "AWS/DynamoDB": {"AccountMaxReads", "AccountMaxTableLevelReads", "AccountMaxTableLevelWrites", "AccountMaxWrites", "AccountProvisionedReadCapacityUtilization", "AccountProvisionedWriteCapacityUtilization", "AgeOfOldestUnreplicatedRecord", "ConditionalCheckFailedRequests", "ConsumedChangeDataCaptureUnits", "ConsumedReadCapacityUnits", "ConsumedWriteCapacityUnits", "FailedToReplicateRecordCount", "MaxProvisionedTableReadCapacityUtilization", "MaxProvisionedTableWriteCapacityUtilization", "OnlineIndexConsumedWriteCapacity", "OnlineIndexPercentageProgress", "OnlineIndexThrottleEvents", "PendingReplicationCount", "ProvisionedReadCapacityUnits", "ProvisionedWriteCapacityUnits", "ReadThrottleEvents", "ReplicationLatency", "ReturnedBytes", "ReturnedItemCount", "ReturnedRecordsCount", "SuccessfulRequestLatency", "SystemErrors", "TimeToLiveDeletedItemCount", "ThrottledPutRecordCount", "ThrottledRequests", "TransactionConflict", "UserErrors", "WriteThrottleEvents"}, "AWS/EBS": {"BurstBalance", "VolumeConsumedReadWriteOps", "VolumeIdleTime", "VolumeQueueLength", "VolumeReadBytes", "VolumeReadOps", "VolumeThroughputPercentage", "VolumeTotalReadTime", "VolumeTotalWriteTime", "VolumeWriteBytes", "VolumeWriteOps"}, diff --git a/pkg/tsdb/elasticsearch/client/client_test.go b/pkg/tsdb/elasticsearch/client/client_test.go index 0e0bb1bed42..d4c2f66da96 100644 --- a/pkg/tsdb/elasticsearch/client/client_test.go +++ b/pkg/tsdb/elasticsearch/client/client_test.go @@ -11,9 +11,10 @@ import ( "github.com/Masterminds/semver" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/components/simplejson" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/components/simplejson" ) func TestClient_ExecuteMultisearch(t *testing.T) { diff --git a/pkg/tsdb/elasticsearch/client/models.go b/pkg/tsdb/elasticsearch/client/models.go index 8ac01ea8620..b2e212ba720 100644 --- a/pkg/tsdb/elasticsearch/client/models.go +++ b/pkg/tsdb/elasticsearch/client/models.go @@ -236,6 +236,11 @@ type TermsAggregation struct { Missing *string `json:"missing,omitempty"` } +// NestedAggregation represents a nested aggregation +type NestedAggregation struct { + Path string `json:"path"` +} + // ExtendedBounds represents extended bounds type ExtendedBounds struct { Min int64 `json:"min"` diff --git a/pkg/tsdb/elasticsearch/client/search_request.go b/pkg/tsdb/elasticsearch/client/search_request.go index f4a4ce97755..e3122e5974b 100644 --- a/pkg/tsdb/elasticsearch/client/search_request.go +++ b/pkg/tsdb/elasticsearch/client/search_request.go @@ -270,6 +270,7 @@ type AggBuilder interface { Histogram(key, field string, fn func(a *HistogramAgg, b AggBuilder)) AggBuilder DateHistogram(key, field string, fn func(a *DateHistogramAgg, b AggBuilder)) AggBuilder Terms(key, field string, fn func(a *TermsAggregation, b AggBuilder)) AggBuilder + Nested(key, path string, fn func(a *NestedAggregation, b AggBuilder)) AggBuilder Filters(key string, fn func(a *FiltersAggregation, b AggBuilder)) AggBuilder GeoHashGrid(key, field string, fn func(a *GeoHashGridAggregation, b AggBuilder)) AggBuilder Metric(key, metricType, field string, fn func(a *MetricAggregation)) AggBuilder @@ -382,6 +383,26 @@ func (b *aggBuilderImpl) Terms(key, field string, fn func(a *TermsAggregation, b return b } +func (b *aggBuilderImpl) Nested(key, field string, fn func(a *NestedAggregation, b AggBuilder)) AggBuilder { + innerAgg := &NestedAggregation{ + Path: field, + } + aggDef := newAggDef(key, &aggContainer{ + Type: "nested", + Aggregation: innerAgg, + }) + + if fn != nil { + builder := newAggBuilder() + aggDef.builders = append(aggDef.builders, builder) + fn(innerAgg, builder) + } + + b.aggDefs = append(b.aggDefs, aggDef) + + return b +} + func (b *aggBuilderImpl) Filters(key string, fn func(a *FiltersAggregation, b AggBuilder)) AggBuilder { innerAgg := &FiltersAggregation{ Filters: make(map[string]interface{}), diff --git a/pkg/tsdb/elasticsearch/client/search_request_test.go b/pkg/tsdb/elasticsearch/client/search_request_test.go index 618249d1619..f3f1601c9a1 100644 --- a/pkg/tsdb/elasticsearch/client/search_request_test.go +++ b/pkg/tsdb/elasticsearch/client/search_request_test.go @@ -5,9 +5,9 @@ import ( "testing" "time" - "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/components/simplejson" ) func TestSearchRequest(t *testing.T) { diff --git a/pkg/tsdb/elasticsearch/elasticsearch_test.go b/pkg/tsdb/elasticsearch/elasticsearch_test.go index 4de838cddaa..529972963ae 100644 --- a/pkg/tsdb/elasticsearch/elasticsearch_test.go +++ b/pkg/tsdb/elasticsearch/elasticsearch_test.go @@ -5,8 +5,9 @@ import ( "testing" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/infra/httpclient" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/infra/httpclient" ) type datasourceInfo struct { diff --git a/pkg/tsdb/elasticsearch/parse_query.go b/pkg/tsdb/elasticsearch/parse_query.go index e43ea6059cb..db397d945e6 100644 --- a/pkg/tsdb/elasticsearch/parse_query.go +++ b/pkg/tsdb/elasticsearch/parse_query.go @@ -2,6 +2,7 @@ package elasticsearch import ( "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/components/simplejson" ) diff --git a/pkg/tsdb/elasticsearch/querydata_test.go b/pkg/tsdb/elasticsearch/querydata_test.go index 34cc82d28a0..c9cb0d49f43 100644 --- a/pkg/tsdb/elasticsearch/querydata_test.go +++ b/pkg/tsdb/elasticsearch/querydata_test.go @@ -11,6 +11,7 @@ import ( "github.com/Masterminds/semver" "github.com/grafana/grafana-plugin-sdk-go/backend" + es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client" ) diff --git a/pkg/tsdb/elasticsearch/response_parser.go b/pkg/tsdb/elasticsearch/response_parser.go index 50b6adcd822..76a63c12b1d 100644 --- a/pkg/tsdb/elasticsearch/response_parser.go +++ b/pkg/tsdb/elasticsearch/response_parser.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/components/simplejson" es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client" ) @@ -22,6 +23,7 @@ const ( topMetricsType = "top_metrics" // Bucket types dateHistType = "date_histogram" + nestedType = "nested" histogramType = "histogram" filtersType = "filters" termsType = "terms" @@ -84,6 +86,13 @@ func processBuckets(aggs map[string]interface{}, target *Query, if aggDef == nil { continue } + if aggDef.Type == nestedType { + err = processBuckets(esAgg.MustMap(), target, queryResult, props, depth+1) + if err != nil { + return err + } + continue + } if depth == maxDepth { if aggDef.Type == dateHistType { diff --git a/pkg/tsdb/elasticsearch/response_parser_test.go b/pkg/tsdb/elasticsearch/response_parser_test.go index 8108da42167..5be058c9bea 100644 --- a/pkg/tsdb/elasticsearch/response_parser_test.go +++ b/pkg/tsdb/elasticsearch/response_parser_test.go @@ -9,9 +9,10 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/experimental" - es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client" ) var update = flag.Bool("update", true, "update golden files") diff --git a/pkg/tsdb/elasticsearch/time_series_query.go b/pkg/tsdb/elasticsearch/time_series_query.go index d1e50e5f169..f8fc378a7e3 100644 --- a/pkg/tsdb/elasticsearch/time_series_query.go +++ b/pkg/tsdb/elasticsearch/time_series_query.go @@ -7,6 +7,7 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/components/simplejson" es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client" ) @@ -244,6 +245,14 @@ func addTermsAgg(aggBuilder es.AggBuilder, bucketAgg *BucketAgg, metrics []*Metr return aggBuilder } +func addNestedAgg(aggBuilder es.AggBuilder, bucketAgg *BucketAgg) es.AggBuilder { + aggBuilder.Nested(bucketAgg.ID, bucketAgg.Field, func(a *es.NestedAggregation, b es.AggBuilder) { + aggBuilder = b + }) + + return aggBuilder +} + func addFiltersAgg(aggBuilder es.AggBuilder, bucketAgg *BucketAgg) es.AggBuilder { filters := make(map[string]interface{}) for _, filter := range bucketAgg.Settings.Get("filters").MustArray() { @@ -361,6 +370,8 @@ func processTimeSeriesQuery(q *Query, b *es.SearchRequestBuilder, from, to int64 aggBuilder = addTermsAgg(aggBuilder, bucketAgg, q.Metrics) case geohashGridType: aggBuilder = addGeoHashGridAgg(aggBuilder, bucketAgg) + case nestedType: + aggBuilder = addNestedAgg(aggBuilder, bucketAgg) } } diff --git a/pkg/tsdb/elasticsearch/time_series_query_test.go b/pkg/tsdb/elasticsearch/time_series_query_test.go index c74c4e152cd..a7af8f5368d 100644 --- a/pkg/tsdb/elasticsearch/time_series_query_test.go +++ b/pkg/tsdb/elasticsearch/time_series_query_test.go @@ -6,9 +6,10 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend" - es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client" ) func TestExecuteTimeSeriesQuery(t *testing.T) { diff --git a/pkg/tsdb/grafanads/grafana.go b/pkg/tsdb/grafanads/grafana.go index 5fe344c1423..f79c1893338 100644 --- a/pkg/tsdb/grafanads/grafana.go +++ b/pkg/tsdb/grafanads/grafana.go @@ -9,14 +9,15 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/searchV2" "github.com/grafana/grafana/pkg/services/store" "github.com/grafana/grafana/pkg/tsdb/testdatasource" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" ) // DatasourceName is the string constant used as the datasource name in requests diff --git a/pkg/tsdb/graphite/graphite_test.go b/pkg/tsdb/graphite/graphite_test.go index 88b2f677be9..12336433f39 100644 --- a/pkg/tsdb/graphite/graphite_test.go +++ b/pkg/tsdb/graphite/graphite_test.go @@ -14,9 +14,10 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/grafana/grafana/pkg/components/simplejson" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/components/simplejson" ) func TestFixIntervalFormat(t *testing.T) { diff --git a/pkg/tsdb/influxdb/flux/executor_test.go b/pkg/tsdb/influxdb/flux/executor_test.go index 937f825eb1f..0bb8398d666 100644 --- a/pkg/tsdb/influxdb/flux/executor_test.go +++ b/pkg/tsdb/influxdb/flux/executor_test.go @@ -14,15 +14,14 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana-plugin-sdk-go/experimental" + influxdb2 "github.com/influxdata/influxdb-client-go/v2" + "github.com/influxdata/influxdb-client-go/v2/api" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/xorcare/pointer" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/tsdb/influxdb/models" - - influxdb2 "github.com/influxdata/influxdb-client-go/v2" - "github.com/influxdata/influxdb-client-go/v2/api" ) //-------------------------------------------------------------- diff --git a/pkg/tsdb/influxdb/flux/query_models.go b/pkg/tsdb/influxdb/flux/query_models.go index 8633229282c..3a97efeffdf 100644 --- a/pkg/tsdb/influxdb/flux/query_models.go +++ b/pkg/tsdb/influxdb/flux/query_models.go @@ -6,6 +6,7 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/tsdb/influxdb/models" ) diff --git a/pkg/tsdb/influxdb/model_parser.go b/pkg/tsdb/influxdb/model_parser.go index 9b8930d0bd9..d33e2aec34c 100644 --- a/pkg/tsdb/influxdb/model_parser.go +++ b/pkg/tsdb/influxdb/model_parser.go @@ -6,6 +6,7 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/components/simplejson" ) diff --git a/pkg/tsdb/influxdb/query.go b/pkg/tsdb/influxdb/query.go index abf160edd15..8ee02dda628 100644 --- a/pkg/tsdb/influxdb/query.go +++ b/pkg/tsdb/influxdb/query.go @@ -8,6 +8,7 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/tsdb/intervalv2" ) diff --git a/pkg/tsdb/loki/api.go b/pkg/tsdb/loki/api.go index a8835da56af..2e87490042a 100644 --- a/pkg/tsdb/loki/api.go +++ b/pkg/tsdb/loki/api.go @@ -12,9 +12,10 @@ import ( "strconv" "github.com/grafana/grafana-plugin-sdk-go/data" + jsoniter "github.com/json-iterator/go" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/util/converter" - jsoniter "github.com/json-iterator/go" ) type LokiAPI struct { @@ -85,8 +86,11 @@ func makeDataRequest(ctx context.Context, lokiDsUrl string, query lokiQuery) (*h return nil, err } - if query.VolumeQuery { - req.Header.Set("X-Query-Tags", "Source=logvolhist") + if query.SupportingQueryType != SupportingQueryNone { + value := getSupportingQueryHeaderValue(req, query.SupportingQueryType) + if value != "" { + req.Header.Set("X-Query-Tags", "Source="+value) + } } return req, nil @@ -223,3 +227,18 @@ func (api *LokiAPI) RawQuery(ctx context.Context, resourcePath string) (RawLokiR return encodedBytes, nil } + +func getSupportingQueryHeaderValue(req *http.Request, supportingQueryType SupportingQueryType) string { + value := "" + switch supportingQueryType { + case SupportingQueryLogsVolume: + value = "logvolhist" + case SupportingQueryLogsSample: + value = "logsample" + case SupportingQueryDataSample: + value = "datasample" + default: //ignore + } + + return value +} diff --git a/pkg/tsdb/loki/api_test.go b/pkg/tsdb/loki/api_test.go index 2b64e383bf4..b28d159d895 100644 --- a/pkg/tsdb/loki/api_test.go +++ b/pkg/tsdb/loki/api_test.go @@ -28,7 +28,31 @@ func TestApiLogVolume(t *testing.T) { require.Equal(t, "Source=logvolhist", req.Header.Get("X-Query-Tags")) }) - _, err := api.DataQuery(context.Background(), lokiQuery{Expr: "", VolumeQuery: true, QueryType: QueryTypeRange}) + _, err := api.DataQuery(context.Background(), lokiQuery{Expr: "", SupportingQueryType: SupportingQueryLogsVolume, QueryType: QueryTypeRange}) + require.NoError(t, err) + require.True(t, called) + }) + + t.Run("logs sample queries should set logs sample http header", func(t *testing.T) { + called := false + api := makeMockedAPI(200, "application/json", response, func(req *http.Request) { + called = true + require.Equal(t, "Source=logsample", req.Header.Get("X-Query-Tags")) + }) + + _, err := api.DataQuery(context.Background(), lokiQuery{Expr: "", SupportingQueryType: SupportingQueryLogsSample, QueryType: QueryTypeRange}) + require.NoError(t, err) + require.True(t, called) + }) + + t.Run("data sample queries should set data sample http header", func(t *testing.T) { + called := false + api := makeMockedAPI(200, "application/json", response, func(req *http.Request) { + called = true + require.Equal(t, "Source=datasample", req.Header.Get("X-Query-Tags")) + }) + + _, err := api.DataQuery(context.Background(), lokiQuery{Expr: "", SupportingQueryType: SupportingQueryDataSample, QueryType: QueryTypeRange}) require.NoError(t, err) require.True(t, called) }) @@ -40,7 +64,7 @@ func TestApiLogVolume(t *testing.T) { require.Equal(t, "", req.Header.Get("X-Query-Tags")) }) - _, err := api.DataQuery(context.Background(), lokiQuery{Expr: "", VolumeQuery: false, QueryType: QueryTypeRange}) + _, err := api.DataQuery(context.Background(), lokiQuery{Expr: "", SupportingQueryType: SupportingQueryNone, QueryType: QueryTypeRange}) require.NoError(t, err) require.True(t, called) }) diff --git a/pkg/tsdb/loki/loki.go b/pkg/tsdb/loki/loki.go index 6925a91c25b..06c57a90924 100644 --- a/pkg/tsdb/loki/loki.go +++ b/pkg/tsdb/loki/loki.go @@ -57,15 +57,15 @@ type datasourceInfo struct { } type QueryJSONModel struct { - QueryType string `json:"queryType"` - Expr string `json:"expr"` - Direction string `json:"direction"` - LegendFormat string `json:"legendFormat"` - Interval string `json:"interval"` - IntervalMS int `json:"intervalMS"` - Resolution int64 `json:"resolution"` - MaxLines int `json:"maxLines"` - VolumeQuery bool `json:"volumeQuery"` + QueryType string `json:"queryType"` + Expr string `json:"expr"` + Direction string `json:"direction"` + LegendFormat string `json:"legendFormat"` + Interval string `json:"interval"` + IntervalMS int `json:"intervalMS"` + Resolution int64 `json:"resolution"` + MaxLines int `json:"maxLines"` + SupportingQueryType string `json:"supportingQueryType"` } func parseQueryModel(raw json.RawMessage) (*QueryJSONModel, error) { @@ -112,7 +112,8 @@ func callResource(ctx context.Context, req *backend.CallResourceRequest, sender } if (!strings.HasPrefix(url, "labels?")) && (!strings.HasPrefix(url, "label/")) && // the `/label/$label_name/values` form - (!strings.HasPrefix(url, "series?")) { + (!strings.HasPrefix(url, "series?")) && + (!strings.HasPrefix(url, "index/stats?")) { return fmt.Errorf("invalid resource URL: %s", url) } lokiURL := fmt.Sprintf("/loki/api/v1/%s", url) diff --git a/pkg/tsdb/loki/parse_query.go b/pkg/tsdb/loki/parse_query.go index 80f0c0cbc27..b4b1f889088 100644 --- a/pkg/tsdb/loki/parse_query.go +++ b/pkg/tsdb/loki/parse_query.go @@ -8,6 +8,7 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/tsdb/intervalv2" ) @@ -82,6 +83,21 @@ func parseDirection(jsonValue string) (Direction, error) { } } +func parseSupportingQueryType(jsonValue string) (SupportingQueryType, error) { + switch jsonValue { + case "logsVolume": + return SupportingQueryLogsVolume, nil + case "logsSample": + return SupportingQueryLogsSample, nil + case "dataSample": + return SupportingQueryDataSample, nil + case "": + return SupportingQueryNone, nil + default: + return SupportingQueryNone, fmt.Errorf("invalid supportingQueryType: %s", jsonValue) + } +} + func parseQuery(queryContext *backend.QueryDataRequest) ([]*lokiQuery, error) { qs := []*lokiQuery{} for _, query := range queryContext.Queries { @@ -115,17 +131,22 @@ func parseQuery(queryContext *backend.QueryDataRequest) ([]*lokiQuery, error) { return nil, err } + supportingQueryType, err := parseSupportingQueryType(model.SupportingQueryType) + if err != nil { + return nil, err + } + qs = append(qs, &lokiQuery{ - Expr: expr, - QueryType: queryType, - Direction: direction, - Step: step, - MaxLines: model.MaxLines, - LegendFormat: model.LegendFormat, - Start: start, - End: end, - RefID: query.RefID, - VolumeQuery: model.VolumeQuery, + Expr: expr, + QueryType: queryType, + Direction: direction, + Step: step, + MaxLines: model.MaxLines, + LegendFormat: model.LegendFormat, + Start: start, + End: end, + RefID: query.RefID, + SupportingQueryType: supportingQueryType, }) } diff --git a/pkg/tsdb/loki/types.go b/pkg/tsdb/loki/types.go index 9d5fcead26d..c2f1b130fb5 100644 --- a/pkg/tsdb/loki/types.go +++ b/pkg/tsdb/loki/types.go @@ -3,12 +3,20 @@ package loki import "time" type QueryType string +type SupportingQueryType string const ( QueryTypeRange QueryType = "range" QueryTypeInstant QueryType = "instant" ) +const ( + SupportingQueryLogsVolume SupportingQueryType = "logsVolume" + SupportingQueryLogsSample SupportingQueryType = "logsSample" + SupportingQueryDataSample SupportingQueryType = "dataSample" + SupportingQueryNone SupportingQueryType = "none" +) + type Direction string const ( @@ -17,14 +25,14 @@ const ( ) type lokiQuery struct { - Expr string - QueryType QueryType - Direction Direction - Step time.Duration - MaxLines int - LegendFormat string - Start time.Time - End time.Time - RefID string - VolumeQuery bool + Expr string + QueryType QueryType + Direction Direction + Step time.Duration + MaxLines int + LegendFormat string + Start time.Time + End time.Time + RefID string + SupportingQueryType SupportingQueryType } diff --git a/pkg/tsdb/prometheus/azureauth/azure.go b/pkg/tsdb/prometheus/azureauth/azure.go index b80bfcf41cc..2bb7711ac82 100644 --- a/pkg/tsdb/prometheus/azureauth/azure.go +++ b/pkg/tsdb/prometheus/azureauth/azure.go @@ -10,8 +10,8 @@ import ( "github.com/grafana/grafana-azure-sdk-go/azsettings" "github.com/grafana/grafana-plugin-sdk-go/backend" sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" - "github.com/grafana/grafana/pkg/tsdb/prometheus/utils" + "github.com/grafana/grafana/pkg/tsdb/prometheus/utils" "github.com/grafana/grafana/pkg/util/maputil" ) diff --git a/pkg/tsdb/prometheus/azureauth/azure_test.go b/pkg/tsdb/prometheus/azureauth/azure_test.go index 8888c13cdec..e6037dfc09f 100644 --- a/pkg/tsdb/prometheus/azureauth/azure_test.go +++ b/pkg/tsdb/prometheus/azureauth/azure_test.go @@ -6,10 +6,10 @@ import ( "github.com/grafana/grafana-azure-sdk-go/azsettings" "github.com/grafana/grafana-plugin-sdk-go/backend" sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" - - "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/setting" ) func TestConfigureAzureAuthentication(t *testing.T) { diff --git a/pkg/tsdb/prometheus/client/client.go b/pkg/tsdb/prometheus/client/client.go index d99d229509b..47a5969d373 100644 --- a/pkg/tsdb/prometheus/client/client.go +++ b/pkg/tsdb/prometheus/client/client.go @@ -12,6 +12,7 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/tsdb/prometheus/models" ) diff --git a/pkg/tsdb/prometheus/client/client_test.go b/pkg/tsdb/prometheus/client/client_test.go index e9928909e93..920baf81dfb 100644 --- a/pkg/tsdb/prometheus/client/client_test.go +++ b/pkg/tsdb/prometheus/client/client_test.go @@ -8,9 +8,10 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" "github.com/grafana/grafana/pkg/tsdb/prometheus/models" - "github.com/stretchr/testify/require" ) type MockDoer struct { diff --git a/pkg/tsdb/prometheus/client/transport.go b/pkg/tsdb/prometheus/client/transport.go index 5e4cccd1038..e74437efee0 100644 --- a/pkg/tsdb/prometheus/client/transport.go +++ b/pkg/tsdb/prometheus/client/transport.go @@ -6,6 +6,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tsdb/prometheus/azureauth" diff --git a/pkg/tsdb/prometheus/client/transport_test.go b/pkg/tsdb/prometheus/client/transport_test.go index 945e0ada0dc..67f0e92b49d 100644 --- a/pkg/tsdb/prometheus/client/transport_test.go +++ b/pkg/tsdb/prometheus/client/transport_test.go @@ -5,9 +5,10 @@ import ( "github.com/grafana/grafana-azure-sdk-go/azsettings" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/log/logtest" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/require" ) func TestCreateTransportOptions(t *testing.T) { diff --git a/pkg/tsdb/prometheus/middleware/custom_query_params.go b/pkg/tsdb/prometheus/middleware/custom_query_params.go index b352c2f6a0d..88c9ceeb384 100644 --- a/pkg/tsdb/prometheus/middleware/custom_query_params.go +++ b/pkg/tsdb/prometheus/middleware/custom_query_params.go @@ -5,6 +5,7 @@ import ( "net/url" sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" + "github.com/grafana/grafana/pkg/infra/log" ) diff --git a/pkg/tsdb/prometheus/middleware/custom_query_params_test.go b/pkg/tsdb/prometheus/middleware/custom_query_params_test.go index fd1e0e22e0a..b242add37a5 100644 --- a/pkg/tsdb/prometheus/middleware/custom_query_params_test.go +++ b/pkg/tsdb/prometheus/middleware/custom_query_params_test.go @@ -7,8 +7,9 @@ import ( "testing" "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" - "github.com/grafana/grafana/pkg/infra/log" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/infra/log" ) func TestCustomQueryParametersMiddleware(t *testing.T) { diff --git a/pkg/tsdb/prometheus/middleware/force_http_get.go b/pkg/tsdb/prometheus/middleware/force_http_get.go index 89061609478..38fc8433607 100644 --- a/pkg/tsdb/prometheus/middleware/force_http_get.go +++ b/pkg/tsdb/prometheus/middleware/force_http_get.go @@ -4,6 +4,7 @@ import ( "net/http" sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" + "github.com/grafana/grafana/pkg/infra/log" ) diff --git a/pkg/tsdb/prometheus/middleware/force_http_get_test.go b/pkg/tsdb/prometheus/middleware/force_http_get_test.go index 21cadae7290..8eb96121dc0 100644 --- a/pkg/tsdb/prometheus/middleware/force_http_get_test.go +++ b/pkg/tsdb/prometheus/middleware/force_http_get_test.go @@ -5,8 +5,9 @@ import ( "testing" "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" - "github.com/grafana/grafana/pkg/infra/log" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/infra/log" ) func TestEnsureHttpMethodMiddleware(t *testing.T) { diff --git a/pkg/tsdb/prometheus/models/query.go b/pkg/tsdb/prometheus/models/query.go index e69bcb9b226..233d8fddc50 100644 --- a/pkg/tsdb/prometheus/models/query.go +++ b/pkg/tsdb/prometheus/models/query.go @@ -8,6 +8,7 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/tsdb/intervalv2" ) diff --git a/pkg/tsdb/prometheus/models/query_test.go b/pkg/tsdb/prometheus/models/query_test.go index fc0efbcb123..dd30c5f0b24 100644 --- a/pkg/tsdb/prometheus/models/query_test.go +++ b/pkg/tsdb/prometheus/models/query_test.go @@ -6,9 +6,10 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/tsdb/intervalv2" "github.com/grafana/grafana/pkg/tsdb/prometheus/models" - "github.com/stretchr/testify/require" ) var ( diff --git a/pkg/tsdb/prometheus/prometheus.go b/pkg/tsdb/prometheus/prometheus.go index e03f7263733..cde81f9968f 100644 --- a/pkg/tsdb/prometheus/prometheus.go +++ b/pkg/tsdb/prometheus/prometheus.go @@ -10,6 +10,9 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/datasource" "github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" + "github.com/patrickmn/go-cache" + apiv1 "github.com/prometheus/client_golang/api/prometheus/v1" + "github.com/grafana/grafana/pkg/infra/httpclient" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" @@ -18,8 +21,6 @@ import ( "github.com/grafana/grafana/pkg/tsdb/prometheus/client" "github.com/grafana/grafana/pkg/tsdb/prometheus/querydata" "github.com/grafana/grafana/pkg/tsdb/prometheus/resource" - "github.com/patrickmn/go-cache" - apiv1 "github.com/prometheus/client_golang/api/prometheus/v1" ) var plog = log.New("tsdb.prometheus") diff --git a/pkg/tsdb/prometheus/prometheus_test.go b/pkg/tsdb/prometheus/prometheus_test.go index 5f9969ce237..ee2b9a1c289 100644 --- a/pkg/tsdb/prometheus/prometheus_test.go +++ b/pkg/tsdb/prometheus/prometheus_test.go @@ -10,10 +10,11 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/datasource" sdkHttpClient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/httpclient" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/require" ) type fakeSender struct{} diff --git a/pkg/tsdb/prometheus/querydata/exemplar/sampler_stddev_test.go b/pkg/tsdb/prometheus/querydata/exemplar/sampler_stddev_test.go index 994abdd551c..b96f6d63b28 100644 --- a/pkg/tsdb/prometheus/querydata/exemplar/sampler_stddev_test.go +++ b/pkg/tsdb/prometheus/querydata/exemplar/sampler_stddev_test.go @@ -5,6 +5,7 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/experimental" + "github.com/grafana/grafana/pkg/tsdb/prometheus/models" "github.com/grafana/grafana/pkg/tsdb/prometheus/querydata/exemplar" ) diff --git a/pkg/tsdb/prometheus/querydata/exemplar/sampler_test.go b/pkg/tsdb/prometheus/querydata/exemplar/sampler_test.go index 3387b16e34b..f071f1b9025 100644 --- a/pkg/tsdb/prometheus/querydata/exemplar/sampler_test.go +++ b/pkg/tsdb/prometheus/querydata/exemplar/sampler_test.go @@ -5,6 +5,7 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/experimental" + "github.com/grafana/grafana/pkg/tsdb/prometheus/models" "github.com/grafana/grafana/pkg/tsdb/prometheus/querydata/exemplar" ) diff --git a/pkg/tsdb/prometheus/querydata/framing_bench_test.go b/pkg/tsdb/prometheus/querydata/framing_bench_test.go index dee98077e7d..7f319ea72da 100644 --- a/pkg/tsdb/prometheus/querydata/framing_bench_test.go +++ b/pkg/tsdb/prometheus/querydata/framing_bench_test.go @@ -15,8 +15,9 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/tsdb/prometheus/models" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/tsdb/prometheus/models" ) // when memory-profiling this benchmark, these commands are recommended: diff --git a/pkg/tsdb/prometheus/querydata/framing_test.go b/pkg/tsdb/prometheus/querydata/framing_test.go index 40d96ea830d..09355702fc2 100644 --- a/pkg/tsdb/prometheus/querydata/framing_test.go +++ b/pkg/tsdb/prometheus/querydata/framing_test.go @@ -12,10 +12,9 @@ import ( "testing" "time" - "github.com/stretchr/testify/require" - "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/experimental" + "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/tsdb/prometheus/models" ) diff --git a/pkg/tsdb/prometheus/querydata/request_test.go b/pkg/tsdb/prometheus/querydata/request_test.go index 8005790781a..7ab268b4e7b 100644 --- a/pkg/tsdb/prometheus/querydata/request_test.go +++ b/pkg/tsdb/prometheus/querydata/request_test.go @@ -13,7 +13,6 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/grafana/grafana/pkg/tsdb/prometheus/client" apiv1 "github.com/prometheus/client_golang/api/prometheus/v1" p "github.com/prometheus/common/model" "github.com/stretchr/testify/require" @@ -22,6 +21,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log/logtest" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tsdb/prometheus/client" "github.com/grafana/grafana/pkg/tsdb/prometheus/models" "github.com/grafana/grafana/pkg/tsdb/prometheus/querydata" ) diff --git a/pkg/tsdb/prometheus/utils/utils.go b/pkg/tsdb/prometheus/utils/utils.go index 8930a214f91..a4e64c2819e 100644 --- a/pkg/tsdb/prometheus/utils/utils.go +++ b/pkg/tsdb/prometheus/utils/utils.go @@ -6,8 +6,9 @@ import ( "fmt" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/infra/tracing" "go.opentelemetry.io/otel/attribute" + + "github.com/grafana/grafana/pkg/infra/tracing" ) // GetJsonData just gets the json in easier to work with type. It's used on multiple places which isn't super effective diff --git a/pkg/util/proxyutil/proxyutil_test.go b/pkg/util/proxyutil/proxyutil_test.go index 95cbf69cf23..24a5d157781 100644 --- a/pkg/util/proxyutil/proxyutil_test.go +++ b/pkg/util/proxyutil/proxyutil_test.go @@ -4,8 +4,9 @@ import ( "net/http" "testing" - "github.com/grafana/grafana/pkg/services/user" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/services/user" ) func TestPrepareProxyRequest(t *testing.T) { diff --git a/pkg/util/proxyutil/reverse_proxy_test.go b/pkg/util/proxyutil/reverse_proxy_test.go index 6d3376a421f..0bab66b9b75 100644 --- a/pkg/util/proxyutil/reverse_proxy_test.go +++ b/pkg/util/proxyutil/reverse_proxy_test.go @@ -8,9 +8,10 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/contexthandler" - "github.com/stretchr/testify/require" ) func TestReverseProxy(t *testing.T) { diff --git a/pkg/web/macaron.go b/pkg/web/macaron.go index 774f4d2d499..758b42309b5 100644 --- a/pkg/web/macaron.go +++ b/pkg/web/macaron.go @@ -19,11 +19,10 @@ package web import ( - _ "unsafe" - "context" "net/http" "strings" + _ "unsafe" ) const _VERSION = "1.3.4.0805" diff --git a/pkg/web/webtest/webtest.go b/pkg/web/webtest/webtest.go index d76f6b56b5d..36d477ee673 100644 --- a/pkg/web/webtest/webtest.go +++ b/pkg/web/webtest/webtest.go @@ -8,15 +8,16 @@ import ( "testing" "github.com/google/uuid" + "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/web" ) -var requests = map[string]*models.ReqContext{} +var requests = map[string]*contextmodel.ReqContext{} type Server struct { t testing.TB @@ -30,7 +31,7 @@ func NewServer(t testing.TB, routeRegister routing.RouteRegister) *Server { t.Helper() m := web.New() - initCtx := &models.ReqContext{} + initCtx := &contextmodel.ReqContext{} m.Use(func(c *web.Context) { initCtx.Context = c initCtx.Logger = log.New("api-test") @@ -104,20 +105,20 @@ func requestIdentifierFromRequest(req *http.Request) string { return req.Header.Get("X-GRAFANA-WEB-TEST-ID") } -func RequestWithWebContext(req *http.Request, c *models.ReqContext) *http.Request { +func RequestWithWebContext(req *http.Request, c *contextmodel.ReqContext) *http.Request { reqID := requestIdentifierFromRequest(req) requests[reqID] = c return req } func RequestWithSignedInUser(req *http.Request, user *user.SignedInUser) *http.Request { - return RequestWithWebContext(req, &models.ReqContext{ + return RequestWithWebContext(req, &contextmodel.ReqContext{ SignedInUser: user, IsSignedIn: true, }) } -func requestContextFromRequest(req *http.Request) *models.ReqContext { +func requestContextFromRequest(req *http.Request) *contextmodel.ReqContext { reqID := requestIdentifierFromRequest(req) val, exists := requests[reqID] if !exists { @@ -130,7 +131,7 @@ func requestContextFromRequest(req *http.Request) *models.ReqContext { func requestContextMiddleware() web.Middleware { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - c := ctxkey.Get(r.Context()).(*models.ReqContext) + c := ctxkey.Get(r.Context()).(*contextmodel.ReqContext) ctx := requestContextFromRequest(r) if ctx != nil { diff --git a/pkg/web/webtest/webtest_test.go b/pkg/web/webtest/webtest_test.go index f184f0e6bc5..c1d696abe2a 100644 --- a/pkg/web/webtest/webtest_test.go +++ b/pkg/web/webtest/webtest_test.go @@ -7,17 +7,18 @@ import ( "strings" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" - "github.com/grafana/grafana/pkg/models" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/user" - "github.com/stretchr/testify/require" ) func TestServer(t *testing.T) { routeRegister := routing.NewRouteRegister() var actualRequest *http.Request - routeRegister.Post("/api", routing.Wrap(func(c *models.ReqContext) response.Response { + routeRegister.Post("/api", routing.Wrap(func(c *contextmodel.ReqContext) response.Response { actualRequest = c.Req return response.JSON(http.StatusOK, c.SignedInUser) })) @@ -68,7 +69,7 @@ func verifyRequest(t *testing.T, s *Server, req *http.Request, expectedBody stri require.NotEmpty(t, requestIdentifierFromRequest(req)) - req = RequestWithWebContext(req, &models.ReqContext{ + req = RequestWithWebContext(req, &contextmodel.ReqContext{ IsSignedIn: true, }) require.NotNil(t, req) @@ -79,7 +80,7 @@ func verifyRequest(t *testing.T, s *Server, req *http.Request, expectedBody stri func TestServerClient(t *testing.T) { routeRegister := routing.NewRouteRegister() - routeRegister.Get("/test", routing.Wrap(func(c *models.ReqContext) response.Response { + routeRegister.Get("/test", routing.Wrap(func(c *contextmodel.ReqContext) response.Response { return response.JSON(http.StatusOK, c.SignedInUser) })) @@ -87,7 +88,7 @@ func TestServerClient(t *testing.T) { t.Run("Making a request with user 1 should return user 1 as signed in user", func(t *testing.T) { req := s.NewRequest(http.MethodGet, "/test", nil) - req = RequestWithWebContext(req, &models.ReqContext{ + req = RequestWithWebContext(req, &contextmodel.ReqContext{ SignedInUser: &user.SignedInUser{ UserID: 1, }, @@ -109,7 +110,7 @@ func TestServerClient(t *testing.T) { t.Run("Making a request with user 2 should return user 2 as signed in user", func(t *testing.T) { req := s.NewRequest(http.MethodGet, "/test", nil) - req = RequestWithWebContext(req, &models.ReqContext{ + req = RequestWithWebContext(req, &contextmodel.ReqContext{ SignedInUser: &user.SignedInUser{ UserID: 2, }, diff --git a/public/api-merged.json b/public/api-merged.json index 8f18508d55e..b8b4d81b4d3 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -2496,6 +2496,35 @@ } } }, + "/api/v1/provisioning/alert-rules/export": { + "get": { + "tags": [ + "provisioning" + ], + "summary": "Export all alert rules in provisioning file format.", + "operationId": "RouteGetAlertRulesExport", + "parameters": [ + { + "type": "boolean", + "default": false, + "description": "Whether to initiate a download of the file or not.", + "name": "download", + "in": "query" + } + ], + "responses": { + "200": { + "description": "AlertingFileExport", + "schema": { + "$ref": "#/definitions/AlertingFileExport" + } + }, + "404": { + "description": " Not found." + } + } + } + }, "/api/v1/provisioning/alert-rules/{UID}": { "get": { "tags": [ @@ -2591,6 +2620,47 @@ } } }, + "/api/v1/provisioning/alert-rules/{UID}/export": { + "get": { + "produces": [ + "application/json", + "application/yaml", + "text/yaml" + ], + "tags": [ + "provisioning" + ], + "summary": "Export an alert rule in provisioning file format.", + "operationId": "RouteGetAlertRuleExport", + "parameters": [ + { + "type": "string", + "description": "Alert rule UID", + "name": "UID", + "in": "path", + "required": true + }, + { + "type": "boolean", + "default": false, + "description": "Whether to initiate a download of the file or not.", + "name": "download", + "in": "query" + } + ], + "responses": { + "200": { + "description": "AlertingFileExport", + "schema": { + "$ref": "#/definitions/AlertingFileExport" + } + }, + "404": { + "description": " Not found." + } + } + } + }, "/api/v1/provisioning/contact-points": { "get": { "tags": [ @@ -2794,6 +2864,52 @@ } } }, + "/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}/export": { + "get": { + "produces": [ + "application/json", + "application/yaml", + "text/yaml" + ], + "tags": [ + "provisioning" + ], + "summary": "Export an alert rule group in provisioning file format.", + "operationId": "RouteGetAlertRuleGroupExport", + "parameters": [ + { + "type": "string", + "name": "FolderUID", + "in": "path", + "required": true + }, + { + "type": "string", + "name": "Group", + "in": "path", + "required": true + }, + { + "type": "boolean", + "default": false, + "description": "Whether to initiate a download of the file or not.", + "name": "download", + "in": "query" + } + ], + "responses": { + "200": { + "description": "AlertingFileExport", + "schema": { + "$ref": "#/definitions/AlertingFileExport" + } + }, + "404": { + "description": " Not found." + } + } + } + }, "/api/v1/provisioning/mute-timings": { "get": { "tags": [ @@ -11044,6 +11160,28 @@ } } }, + "AlertQueryExport": { + "type": "object", + "title": "AlertQueryExport is the provisioned export of models.AlertQuery.", + "properties": { + "datasourceUid": { + "type": "string" + }, + "model": { + "type": "object", + "additionalProperties": false + }, + "queryType": { + "type": "string" + }, + "refId": { + "type": "string" + }, + "relativeTimeRange": { + "$ref": "#/definitions/RelativeTimeRange" + } + } + }, "AlertResponse": { "type": "object", "required": [ @@ -11064,6 +11202,65 @@ } } }, + "AlertRuleExport": { + "type": "object", + "title": "AlertRuleExport is the provisioned file export of models.AlertRule.", + "properties": { + "annotations": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "condition": { + "type": "string" + }, + "dasboardUid": { + "type": "string" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/AlertQueryExport" + } + }, + "execErrState": { + "type": "string", + "enum": [ + "Alerting", + "Error", + "OK" + ] + }, + "for": { + "$ref": "#/definitions/Duration" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "noDataState": { + "type": "string", + "enum": [ + "Alerting", + "NoData", + "OK" + ] + }, + "panelId": { + "type": "integer", + "format": "int64" + }, + "title": { + "type": "string" + }, + "uid": { + "type": "string" + } + } + }, "AlertRuleGroup": { "type": "object", "properties": { @@ -11085,6 +11282,31 @@ } } }, + "AlertRuleGroupExport": { + "type": "object", + "title": "AlertRuleGroupExport is the provisioned file export of AlertRuleGroupV1.", + "properties": { + "folder": { + "type": "string" + }, + "interval": { + "$ref": "#/definitions/Duration" + }, + "name": { + "type": "string" + }, + "orgId": { + "type": "integer", + "format": "int64" + }, + "rules": { + "type": "array", + "items": { + "$ref": "#/definitions/AlertRuleExport" + } + } + } + }, "AlertRuleGroupMetadata": { "type": "object", "properties": { @@ -11174,6 +11396,22 @@ } } }, + "AlertingFileExport": { + "type": "object", + "title": "AlertingFileExport is the full provisioned file export.", + "properties": { + "apiVersion": { + "type": "integer", + "format": "int64" + }, + "groups": { + "type": "array", + "items": { + "$ref": "#/definitions/AlertRuleGroupExport" + } + } + } + }, "AlertingRule": { "description": "adapted from cortex", "type": "object", @@ -15765,6 +16003,10 @@ "type": "integer", "format": "int64" }, + "isPaused": { + "type": "boolean", + "example": true + }, "labels": { "type": "object", "additionalProperties": { @@ -17824,9 +18066,8 @@ "type": "string" }, "URL": { - "description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use RawPath, an optional field which only gets\nset if the default encoding is different from Path.\n\nURL's String method uses the EscapedPath method to obtain the path. See the\nEscapedPath method for more details.", "type": "object", - "title": "A URL represents a parsed URL (technically, a URI reference).", + "title": "URL is a custom URL type that allows validation at configuration load time.", "properties": { "ForceQuery": { "type": "boolean" @@ -18692,6 +18933,7 @@ } }, "alertGroups": { + "description": "AlertGroups alert groups", "type": "array", "items": { "$ref": "#/definitions/alertGroup" @@ -18796,7 +19038,6 @@ } }, "gettableAlert": { - "description": "GettableAlert gettable alert", "type": "object", "required": [ "labels", @@ -18852,6 +19093,7 @@ } }, "gettableAlerts": { + "description": "GettableAlerts gettable alerts", "type": "array", "items": { "$ref": "#/definitions/gettableAlert" @@ -18906,13 +19148,13 @@ } }, "gettableSilences": { + "description": "GettableSilences gettable silences", "type": "array", "items": { "$ref": "#/definitions/gettableSilence" } }, "integration": { - "description": "Integration integration", "type": "object", "required": [ "name", @@ -19056,7 +19298,6 @@ } }, "postableSilence": { - "description": "PostableSilence postable silence", "type": "object", "required": [ "comment", diff --git a/public/app/angular/AngularApp.ts b/public/app/angular/AngularApp.ts index a4fb82ec255..2d64500d54b 100644 --- a/public/app/angular/AngularApp.ts +++ b/public/app/angular/AngularApp.ts @@ -43,12 +43,14 @@ export class AngularApp { '$filterProvider', '$httpProvider', '$provide', + '$sceDelegateProvider', ( $controllerProvider: angular.IControllerProvider, $compileProvider: angular.ICompileProvider, $filterProvider: angular.IFilterProvider, $httpProvider: angular.IHttpProvider, - $provide: angular.auto.IProvideService + $provide: angular.auto.IProvideService, + $sceDelegateProvider: angular.ISCEDelegateProvider ) => { if (config.buildInfo.env !== 'development') { $compileProvider.debugInfoEnabled(false); @@ -56,6 +58,10 @@ export class AngularApp { $httpProvider.useApplyAsync(true); + if (Boolean(config.pluginsCDNBaseURL)) { + $sceDelegateProvider.trustedResourceUrlList(['self', `${config.pluginsCDNBaseURL}/**`]); + } + this.registerFunctions.controller = $controllerProvider.register; this.registerFunctions.directive = $compileProvider.directive; this.registerFunctions.factory = $provide.factory; diff --git a/public/app/angular/components/plugin_component.test.ts b/public/app/angular/components/plugin_component.test.ts new file mode 100644 index 00000000000..e5205751fc6 --- /dev/null +++ b/public/app/angular/components/plugin_component.test.ts @@ -0,0 +1,18 @@ +import { config } from '@grafana/runtime'; + +import { relativeTemplateUrlToCDN } from './plugin_component'; + +describe('Plugin Component', () => { + describe('relativeTemplateUrlToCDN()', () => { + it('should create a proper path', () => { + config.pluginsCDNBaseURL = 'http://my-host.com'; + + const templateUrl = 'partials/module.html'; + const baseUrl = 'plugin-cdn/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel'; + const expectedUrl = + 'http://my-host.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/partials/module.html'; + + expect(relativeTemplateUrlToCDN(templateUrl, baseUrl)).toBe(expectedUrl); + }); + }); +}); diff --git a/public/app/angular/components/plugin_component.ts b/public/app/angular/components/plugin_component.ts index f8271c41cec..a22b122c31c 100644 --- a/public/app/angular/components/plugin_component.ts +++ b/public/app/angular/components/plugin_component.ts @@ -8,6 +8,20 @@ import config from 'app/core/config'; import { importPanelPlugin } from '../../features/plugins/importPanelPlugin'; import { importDataSourcePlugin, importAppPlugin } from '../../features/plugins/plugin_loader'; +export function relativeTemplateUrlToCDN(templateUrl: string, baseUrl: string) { + if (!templateUrl) { + return undefined; + } + + // the templateUrl may have already been updated with the hostname + if (templateUrl.startsWith(config.pluginsCDNBaseURL)) { + return templateUrl; + } + + // use the 'plugin-cdn' key to load via cdn + return `${baseUrl.replace('plugin-cdn/', `${config.pluginsCDNBaseURL}/`)}/${templateUrl}`; +} + coreModule.directive('pluginComponent', ['$compile', '$http', '$templateCache', '$location', pluginDirectiveLoader]); function pluginDirectiveLoader($compile: any, $http: any, $templateCache: any, $location: ILocationService) { @@ -31,12 +45,17 @@ function pluginDirectiveLoader($compile: any, $http: any, $templateCache: any, $ if (templateUrl.indexOf('public') === 0) { return templateUrl; } + return baseUrl + '/' + templateUrl; } function getPluginComponentDirective(options: any) { - // handle relative template urls for plugin templates - options.Component.templateUrl = relativeTemplateUrlToAbs(options.Component.templateUrl, options.baseUrl); + if (options.baseUrl.includes('plugin-cdn')) { + options.Component.templateUrl = relativeTemplateUrlToCDN(options.Component.templateUrl, options.baseUrl); + } else { + // handle relative template urls for plugin templates + options.Component.templateUrl = relativeTemplateUrlToAbs(options.Component.templateUrl, options.baseUrl); + } return () => { return { @@ -86,13 +105,17 @@ function pluginDirectiveLoader($compile: any, $http: any, $templateCache: any, $ } if (panelInfo) { - PanelCtrl.templateUrl = relativeTemplateUrlToAbs(PanelCtrl.templateUrl, panelInfo.baseUrl); + if (panelInfo.baseUrl.includes('plugin-cdn')) { + PanelCtrl.templateUrl = relativeTemplateUrlToCDN(PanelCtrl.templateUrl, panelInfo.baseUrl); + } else { + PanelCtrl.templateUrl = relativeTemplateUrlToAbs(PanelCtrl.templateUrl, panelInfo.baseUrl); + } } PanelCtrl.templatePromise = getTemplate(PanelCtrl).then((template: any) => { PanelCtrl.templateUrl = null; PanelCtrl.template = `${template}`; - return componentInfo; + return { ...componentInfo, baseUrl: panelInfo.baseUrl }; }); return PanelCtrl.templatePromise; diff --git a/public/app/core/components/AppChrome/QuickAdd/QuickAdd.test.tsx b/public/app/core/components/AppChrome/QuickAdd/QuickAdd.test.tsx index 3aba09ac311..568ec4af0d8 100644 --- a/public/app/core/components/AppChrome/QuickAdd/QuickAdd.test.tsx +++ b/public/app/core/components/AppChrome/QuickAdd/QuickAdd.test.tsx @@ -4,10 +4,18 @@ import React from 'react'; import { Provider } from 'react-redux'; import { NavModelItem, NavSection } from '@grafana/data'; +import { reportInteraction } from '@grafana/runtime'; import { configureStore } from 'app/store/configureStore'; import { QuickAdd } from './QuickAdd'; +jest.mock('@grafana/runtime', () => { + return { + ...jest.requireActual('@grafana/runtime'), + reportInteraction: jest.fn(), + }; +}); + const setup = () => { const navBarTree: NavModelItem[] = [ { @@ -16,7 +24,7 @@ const setup = () => { id: 'section1', url: 'section1', children: [ - { text: 'New child 1', id: 'child1', url: 'section1/child1', isCreateAction: true }, + { text: 'New child 1', id: 'child1', url: '#', isCreateAction: true }, { text: 'Child2', id: 'child2', url: 'section1/child2' }, ], }, @@ -50,4 +58,15 @@ describe('QuickAdd', () => { expect(screen.getByRole('link', { name: 'New child 1' })).toBeInTheDocument(); expect(screen.getByRole('link', { name: 'New child 3' })).toBeInTheDocument(); }); + + it('reports interaction when a menu item is clicked', async () => { + setup(); + await userEvent.click(screen.getByRole('button', { name: 'New' })); + await userEvent.click(screen.getByRole('link', { name: 'New child 1' })); + + expect(reportInteraction).toHaveBeenCalledWith('grafana_menu_item_clicked', { + url: '#', + from: 'quickadd', + }); + }); }); diff --git a/public/app/core/components/AppChrome/QuickAdd/QuickAdd.tsx b/public/app/core/components/AppChrome/QuickAdd/QuickAdd.tsx index 0ed2337c7ff..ac38c048f0b 100644 --- a/public/app/core/components/AppChrome/QuickAdd/QuickAdd.tsx +++ b/public/app/core/components/AppChrome/QuickAdd/QuickAdd.tsx @@ -2,6 +2,7 @@ import { css } from '@emotion/css'; import React, { useMemo, useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; +import { reportInteraction } from '@grafana/runtime'; import { Menu, Dropdown, useStyles2, useTheme2, ToolbarButton } from '@grafana/ui'; import { useMediaQueryChange } from 'app/core/hooks/useMediaQueryChange'; import { useSelector } from 'app/types'; @@ -32,7 +33,12 @@ export const QuickAdd = ({}: Props) => { return ( {createActions.map((createAction, index) => ( - + reportInteraction('grafana_menu_item_clicked', { url: createAction.url, from: 'quickadd' })} + /> ))} ); diff --git a/public/app/core/components/AppChrome/TopSearchBar.tsx b/public/app/core/components/AppChrome/TopSearchBar.tsx index b6246da47bd..726171f4ddc 100644 --- a/public/app/core/components/AppChrome/TopSearchBar.tsx +++ b/public/app/core/components/AppChrome/TopSearchBar.tsx @@ -79,7 +79,7 @@ const getStyles = (theme: GrafanaTheme2) => ({ justifyContent: 'space-between', [theme.breakpoints.up('sm')]: { - gridTemplateColumns: '1.5fr minmax(200px, 1fr) 1.5fr', // search should not be smaller than 200px + gridTemplateColumns: '1.5fr minmax(240px, 1fr) 1.5fr', // search should not be smaller than 240px display: 'grid', justifyContent: 'flex-start', diff --git a/public/app/core/components/AppChrome/TopSearchBarCommandPaletteTrigger.tsx b/public/app/core/components/AppChrome/TopSearchBarCommandPaletteTrigger.tsx index c67660acaae..08a5c701c51 100644 --- a/public/app/core/components/AppChrome/TopSearchBarCommandPaletteTrigger.tsx +++ b/public/app/core/components/AppChrome/TopSearchBarCommandPaletteTrigger.tsx @@ -36,7 +36,7 @@ export function TopSearchBarCommandPaletteTrigger() { ); @@ -65,7 +65,7 @@ function PretendTextInput({ onClick }: PretendTextInputProps) {
diff --git a/public/app/core/components/NavBar/navBarItem-translations.ts b/public/app/core/components/NavBar/navBarItem-translations.ts index f47058741eb..30e2dfac8e3 100644 --- a/public/app/core/components/NavBar/navBarItem-translations.ts +++ b/public/app/core/components/NavBar/navBarItem-translations.ts @@ -162,7 +162,10 @@ export function getNavSubTitle(navId: string | undefined) { case 'alert-list': return t('nav.alerting-list.subtitle', 'Rules that determine whether an alert will fire'); case 'receivers': - return t('nav.alerting-receivers.subtitle', 'Decide how your contacts are notified when an alert fires'); + return t( + 'nav.alerting-receivers.subtitle', + 'Choose how to notify your contact points when an alert instance fires' + ); case 'am-routes': return t('nav.alerting-am-routes.subtitle', 'Determine how alerts are routed to contact points'); case 'silences': diff --git a/public/app/core/components/NavBar/utils.ts b/public/app/core/components/NavBar/utils.ts index eef4c0cece1..df94b105376 100644 --- a/public/app/core/components/NavBar/utils.ts +++ b/public/app/core/components/NavBar/utils.ts @@ -4,11 +4,10 @@ import { locationUtil, NavModelItem, NavSection } from '@grafana/data'; import { config, reportInteraction } from '@grafana/runtime'; import { t } from 'app/core/internationalization'; import { contextSrv } from 'app/core/services/context_srv'; -import { AccessControlAction } from 'app/types'; import { ShowModalReactEvent } from '../../../types/events'; import appEvents from '../../app_events'; -import { FooterLink, getFooterLinks } from '../Footer/Footer'; +import { getFooterLinks } from '../Footer/Footer'; import { OrgSwitcher } from '../OrgSwitcher'; import { HelpModal } from '../help/HelpModal'; @@ -53,8 +52,8 @@ export const enrichConfigItems = (items: NavModelItem[], location: Location { - const hasAccess = - contextSrv.hasAccess(AccessControlAction.ActionSupportBundlesCreate, contextSrv.isGrafanaAdmin) || - contextSrv.hasAccess(AccessControlAction.ActionSupportBundlesRead, contextSrv.isGrafanaAdmin); - - if (!cfg.supportBundlesEnabled || !hasAccess) { - return []; - } - - return [ - { - target: '_self', - id: 'support-bundle', - text: t('nav.help/support-bundle', 'Support Bundles'), - icon: 'question-circle', - url: '/support-bundles', - }, - ]; -}; - export const enrichWithInteractionTracking = (item: NavModelItem, expandedState: boolean) => { const onClick = item.onClick; item.onClick = () => { diff --git a/public/app/core/components/QueryOperationRow/QueryOperationRow.test.tsx b/public/app/core/components/QueryOperationRow/QueryOperationRow.test.tsx index 2efdd6a2729..3a3db9e0527 100644 --- a/public/app/core/components/QueryOperationRow/QueryOperationRow.test.tsx +++ b/public/app/core/components/QueryOperationRow/QueryOperationRow.test.tsx @@ -32,7 +32,7 @@ describe('QueryOperationRow', () => { const onCloseSpy = jest.fn(); setup({ isOpen: false, onOpen: onOpenSpy, onClose: onCloseSpy }); - const queryRow = screen.getByText(/^test-title$/); + const queryRow = screen.getByRole('button', { name: /^test-title$/ }); expect(queryRow).toBeInTheDocument(); // open row on click diff --git a/public/app/core/components/QueryOperationRow/QueryOperationRow.tsx b/public/app/core/components/QueryOperationRow/QueryOperationRow.tsx index 8b901100558..4c7d826940b 100644 --- a/public/app/core/components/QueryOperationRow/QueryOperationRow.tsx +++ b/public/app/core/components/QueryOperationRow/QueryOperationRow.tsx @@ -90,7 +90,6 @@ export function QueryOperationRow({ }, }; - const titleElement = title && ReactUtils.renderOrCallToRender(title, renderPropArgs); const actionsElement = actions && ReactUtils.renderOrCallToRender(actions, renderPropArgs); const headerElementRendered = headerElement && ReactUtils.renderOrCallToRender(headerElement, renderPropArgs); @@ -112,7 +111,7 @@ export function QueryOperationRow({ isContentVisible={isContentVisible} onRowToggle={onRowToggle} reportDragMousePosition={reportDragMousePosition} - titleElement={titleElement} + title={title} />
{isContentVisible &&
{children}
} @@ -135,7 +134,7 @@ export function QueryOperationRow({ isContentVisible={isContentVisible} onRowToggle={onRowToggle} reportDragMousePosition={reportDragMousePosition} - titleElement={titleElement} + title={title} /> {isContentVisible &&
{children}
} diff --git a/public/app/core/components/QueryOperationRow/QueryOperationRowHeader.tsx b/public/app/core/components/QueryOperationRow/QueryOperationRowHeader.tsx index 3e1873571d7..76df2471d05 100644 --- a/public/app/core/components/QueryOperationRow/QueryOperationRowHeader.tsx +++ b/public/app/core/components/QueryOperationRow/QueryOperationRowHeader.tsx @@ -1,4 +1,5 @@ import { css, cx } from '@emotion/css'; +import { useId } from '@react-aria/utils'; import React, { MouseEventHandler } from 'react'; import { DraggableProvidedDragHandleProps } from 'react-beautiful-dnd'; @@ -14,7 +15,7 @@ interface QueryOperationRowHeaderProps { isContentVisible: boolean; onRowToggle: () => void; reportDragMousePosition: MouseEventHandler; - titleElement?: React.ReactNode; + title?: string; id: string; } @@ -27,10 +28,11 @@ export const QueryOperationRowHeader: React.FC = ( isContentVisible, onRowToggle, reportDragMousePosition, - titleElement, + title, id, }: QueryOperationRowHeaderProps) => { const styles = useStyles2(getStyles); + const titleId = useId(); return (
@@ -44,10 +46,13 @@ export const QueryOperationRowHeader: React.FC = ( type="button" aria-expanded={isContentVisible} aria-controls={id} + aria-labelledby={titleId} /> - {titleElement && ( -
-
{titleElement}
+ {title && ( +
+
+ {title} +
)} {headerElement} @@ -109,13 +114,11 @@ const getStyles = (theme: GrafanaTheme2) => ({ display: flex; align-items: center; flex-grow: 1; - cursor: pointer; overflow: hidden; margin-right: ${theme.spacing(0.5)}; `, title: css` font-weight: ${theme.typography.fontWeightBold}; - color: ${theme.colors.text.link}; margin-left: ${theme.spacing(0.5)}; overflow: hidden; text-overflow: ellipsis; diff --git a/public/app/core/components/SharedPreferences/SharedPreferences.tsx b/public/app/core/components/SharedPreferences/SharedPreferences.tsx index 73b7ee09290..6b4d6904ac7 100644 --- a/public/app/core/components/SharedPreferences/SharedPreferences.tsx +++ b/public/app/core/components/SharedPreferences/SharedPreferences.tsx @@ -71,6 +71,7 @@ export class SharedPreferences extends PureComponent { { value: '', label: t('shared-preferences.theme.default-label', 'Default') }, { value: 'dark', label: t('shared-preferences.theme.dark-label', 'Dark') }, { value: 'light', label: t('shared-preferences.theme.light-label', 'Light') }, + { value: 'system', label: t('shared-preferences.theme.system-label', 'System') }, ]; } diff --git a/public/app/core/components/TimelineChart/utils.ts b/public/app/core/components/TimelineChart/utils.ts index 7351ca00f00..d13700390e6 100644 --- a/public/app/core/components/TimelineChart/utils.ts +++ b/public/app/core/components/TimelineChart/utils.ts @@ -556,9 +556,10 @@ export function getFieldLegendItem(fields: Field[], theme: GrafanaTheme2): VizLe const items: VizLegendItem[] = []; const fieldConfig = fields[0].config; const colorMode = fieldConfig.color?.mode ?? FieldColorModeId.Fixed; + const thresholds = fieldConfig.thresholds; // If thresholds are enabled show each step in the legend - if (colorMode === FieldColorModeId.Thresholds) { + if (colorMode === FieldColorModeId.Thresholds && thresholds?.steps && thresholds.steps.length > 1) { return getThresholdItems(fieldConfig, theme); } diff --git a/public/app/core/services/context_srv.ts b/public/app/core/services/context_srv.ts index 97d2564a3e6..4a7a5fddbe4 100644 --- a/public/app/core/services/context_srv.ts +++ b/public/app/core/services/context_srv.ts @@ -7,14 +7,14 @@ import { CurrentUserInternal } from 'app/types/config'; import config from '../../core/config'; -export class User implements CurrentUserInternal { +export class User implements Omit { isSignedIn: boolean; id: number; login: string; email: string; name: string; externalUserId: string; - lightTheme: boolean; + theme: string; orgCount: number; orgId: number; orgName: string; @@ -43,7 +43,7 @@ export class User implements CurrentUserInternal { this.timezone = ''; this.fiscalYearStartMonth = 0; this.helpFlags1 = 0; - this.lightTheme = false; + this.theme = 'dark'; this.hasEditPermissionInFolders = false; this.email = ''; this.name = ''; diff --git a/public/app/core/services/echo/utils.test.ts b/public/app/core/services/echo/utils.test.ts index 353f32ec936..5d8e1c26491 100644 --- a/public/app/core/services/echo/utils.test.ts +++ b/public/app/core/services/echo/utils.test.ts @@ -8,7 +8,8 @@ const baseUser: CurrentUserDTO = { login: 'myUsername', email: 'email@example.com', name: 'My Name', - lightTheme: false, + theme: 'dark', + lightTheme: false, // deprecated orgCount: 1, orgId: 1, orgName: 'Main Org.', diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts index b48bbbe1650..51b7518e9d2 100644 --- a/public/app/core/services/keybindingSrv.ts +++ b/public/app/core/services/keybindingSrv.ts @@ -39,9 +39,11 @@ export class KeybindingSrv { this.bind('g a', this.openAlerting); this.bind('g p', this.goToProfile); this.bind('g e', this.goToExplore); - this.bind('s o', this.openSearch); + if (!config.featureToggles.topNavCommandPalette) { + this.bind('s o', this.openSearch); + this.bind('f', this.openSearch); + } this.bind('t a', this.makeAbsoluteTime); - this.bind('f', this.openSearch); this.bind('esc', this.exit); this.bindGlobalEsc(); } diff --git a/public/app/features/admin/UserAdminPage.tsx b/public/app/features/admin/UserAdminPage.tsx index dc84ebc0f84..dcf0ba5dee7 100644 --- a/public/app/features/admin/UserAdminPage.tsx +++ b/public/app/features/admin/UserAdminPage.tsx @@ -117,6 +117,7 @@ export class UserAdminPage extends PureComponent { const isGitLabUser = user?.isExternal && user?.authLabels?.includes('GitLab'); const isAuthProxyUser = user?.isExternal && user?.authLabels?.includes('Auth Proxy'); const isAzureADUser = user?.isExternal && user?.authLabels?.includes('AzureAD'); + const isOktaUser = user?.isExternal && user?.authLabels?.includes('Okta'); const isGrafanaComUser = user?.isExternal && user?.authLabels?.includes('grafana.com'); const isUserSynced = !config.auth.DisableSyncLock && @@ -127,6 +128,7 @@ export class UserAdminPage extends PureComponent { isGitLabUser || isOAuthUserWithSkippableSync || isSAMLUser || + isOktaUser || isLDAPUser || isGithubUser || isAzureADUser || @@ -139,6 +141,7 @@ export class UserAdminPage extends PureComponent { (!config.auth.JWTAuthSkipOrgRoleSync && isJWTUser) || // both OAuthSkipOrgRoleUpdateSync and specific provider settings needs to be false for a user to be synced (!config.auth.OAuthSkipOrgRoleUpdateSync && !config.auth.GrafanaComSkipOrgRoleSync && isGrafanaComUser) || + (!config.auth.OAuthSkipOrgRoleUpdateSync && !config.auth.OktaSkipOrgRoleSync && isOktaUser) || (!config.auth.OAuthSkipOrgRoleUpdateSync && !config.auth.GithubSkipOrgRoleSync && isGithubUser) || (!config.auth.OAuthSkipOrgRoleUpdateSync && !config.auth.AzureADSkipOrgRoleSync && isAzureADUser) || (!config.auth.OAuthSkipOrgRoleUpdateSync && !config.auth.GitLabSkipOrgRoleSync && isGitLabUser) || diff --git a/public/app/features/alerting/unified/AmRoutes.test.tsx b/public/app/features/alerting/unified/AmRoutes.test.tsx index f45cd551dd6..c4a30208147 100644 --- a/public/app/features/alerting/unified/AmRoutes.test.tsx +++ b/public/app/features/alerting/unified/AmRoutes.test.tsx @@ -92,8 +92,8 @@ const ui = { editRouteButton: byLabelText('Edit route'), deleteRouteButton: byLabelText('Delete route'), - newPolicyButton: byRole('button', { name: /New policy/ }), - newPolicyCTAButton: byRole('button', { name: /New specific policy/ }), + newPolicyButton: byRole('button', { name: /Add policy/ }), + newPolicyCTAButton: byRole('button', { name: /Add specific policy/ }), savePolicyButton: byRole('button', { name: /save policy/i }), receiverSelect: byTestId('am-receiver-select'), diff --git a/public/app/features/alerting/unified/Home.tsx b/public/app/features/alerting/unified/Home.tsx index f8e094ebbe9..ce1052853e1 100644 --- a/public/app/features/alerting/unified/Home.tsx +++ b/public/app/features/alerting/unified/Home.tsx @@ -55,10 +55,6 @@ export default function Home() {
-
diff --git a/public/app/features/alerting/unified/Receivers.test.tsx b/public/app/features/alerting/unified/Receivers.test.tsx index ed5c53b7357..db972ac2e5c 100644 --- a/public/app/features/alerting/unified/Receivers.test.tsx +++ b/public/app/features/alerting/unified/Receivers.test.tsx @@ -95,9 +95,9 @@ const dataSources = { }; const ui = { - newContactPointButton: byRole('link', { name: /new contact point/i }), + newContactPointButton: byRole('link', { name: /add contact point/i }), saveContactButton: byRole('button', { name: /save contact point/i }), - newContactPointIntegrationButton: byRole('button', { name: /new contact point integration/i }), + newContactPointIntegrationButton: byRole('button', { name: /add contact point integration/i }), testContactPointButton: byRole('button', { name: /Test/ }), testContactPointModal: byRole('heading', { name: /test contact point/i }), customContactPointOption: byRole('radio', { name: /custom/i }), diff --git a/public/app/features/alerting/unified/RuleList.test.tsx b/public/app/features/alerting/unified/RuleList.test.tsx index 44a8508b672..9d2aede224c 100644 --- a/public/app/features/alerting/unified/RuleList.test.tsx +++ b/public/app/features/alerting/unified/RuleList.test.tsx @@ -1,12 +1,12 @@ import { SerializedError } from '@reduxjs/toolkit'; -import { render, waitFor, screen } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; import { Provider } from 'react-redux'; import { Router } from 'react-router-dom'; import { byRole, byTestId, byText } from 'testing-library-selector'; -import { locationService, setDataSourceSrv, logInfo, setBackendSrv } from '@grafana/runtime'; +import { locationService, logInfo, setBackendSrv, setDataSourceSrv } from '@grafana/runtime'; import { backendSrv } from 'app/core/services/backend_srv'; import { contextSrv } from 'app/core/services/context_srv'; import * as ruleActionButtons from 'app/features/alerting/unified/components/rules/RuleActionsButtons'; @@ -121,9 +121,8 @@ const ui = { rulesFilterInput: byTestId('search-query-input'), moreErrorsButton: byRole('button', { name: /more errors/ }), editCloudGroupIcon: byTestId('edit-group'), - - newRuleButton: byRole('link', { name: 'New alert rule' }), - + newRuleButton: byRole('link', { name: 'Create alert rule' }), + exportButton: byRole('button', { name: /export/i }), editGroupModal: { namespaceInput: byRole('textbox', { hidden: true, name: /namespace/i }), ruleGroupInput: byRole('textbox', { name: 'Evaluation group', exact: true }), @@ -681,6 +680,34 @@ describe('RuleList', () => { }); describe('RBAC Enabled', () => { + describe('Export button', () => { + it('Export button should be visible when the user has alert provisioning read permissions', async () => { + enableRBAC(); + + grantUserPermissions([AccessControlAction.AlertingProvisioningRead]); + + mocks.getAllDataSourcesMock.mockReturnValue([]); + setDataSourceSrv(new MockDataSourceSrv({})); + mocks.api.fetchRules.mockResolvedValue([]); + mocks.api.fetchRulerRules.mockResolvedValue({}); + + renderRuleList(); + + expect(ui.exportButton.get()).toBeInTheDocument(); + }); + it('Export button should not be visible when the user has no alert provisioning read permissions', async () => { + enableRBAC(); + + mocks.getAllDataSourcesMock.mockReturnValue([]); + setDataSourceSrv(new MockDataSourceSrv({})); + mocks.api.fetchRules.mockResolvedValue([]); + mocks.api.fetchRulerRules.mockResolvedValue({}); + + renderRuleList(); + + expect(ui.exportButton.query()).not.toBeInTheDocument(); + }); + }); describe('Grafana Managed Alerts', () => { it('New alert button should be visible when the user has alert rule create and folder read permissions and no rules exists', async () => { enableRBAC(); @@ -800,7 +827,7 @@ describe('RuleList', () => { await waitFor(() => expect(mocks.api.fetchRules).toHaveBeenCalledTimes(1)); - const button = screen.getByText('New alert rule'); + const button = screen.getByText('Create alert rule'); button.addEventListener('click', (event) => event.preventDefault(), false); diff --git a/public/app/features/alerting/unified/RuleList.tsx b/public/app/features/alerting/unified/RuleList.tsx index c819a0b81bc..5f0eed1cbf1 100644 --- a/public/app/features/alerting/unified/RuleList.tsx +++ b/public/app/features/alerting/unified/RuleList.tsx @@ -4,6 +4,7 @@ import { useLocation } from 'react-router-dom'; import { useAsyncFn, useInterval } from 'react-use'; import { GrafanaTheme2, urlUtil } from '@grafana/data'; +import { Stack } from '@grafana/experimental'; import { logInfo } from '@grafana/runtime'; import { Button, LinkButton, useStyles2, withErrorBoundary } from '@grafana/ui'; import { useQueryParams } from 'app/core/hooks/useQueryParams'; @@ -32,6 +33,8 @@ const VIEWS = { state: RuleListStateView, }; +const onExport = () => window.open(`/api/v1/provisioning/alert-rules/export?download=true`); + const RuleList = withErrorBoundary( () => { const dispatch = useDispatch(); @@ -43,7 +46,7 @@ const RuleList = withErrorBoundary( const [queryParams] = useQueryParams(); const { filterState, hasActiveFilters } = useRulesFilter(); - const { canCreateGrafanaRules, canCreateCloudRules } = useRulesAccess(); + const { canCreateGrafanaRules, canCreateCloudRules, canReadProvisioning } = useRulesAccess(); const view = VIEWS[queryParams['view'] as keyof typeof VIEWS] ? (queryParams['view'] as keyof typeof VIEWS) @@ -106,15 +109,22 @@ const RuleList = withErrorBoundary( )}
- {(canCreateGrafanaRules || canCreateCloudRules) && ( - logInfo(LogMessages.alertRuleFromScratch)} - > - New alert rule - - )} + + {canReadProvisioning && ( + + )} + {(canCreateGrafanaRules || canCreateCloudRules) && ( + logInfo(LogMessages.alertRuleFromScratch)} + > + Create alert rule + + )} +
)} diff --git a/public/app/features/alerting/unified/RuleViewer.test.tsx b/public/app/features/alerting/unified/RuleViewer.test.tsx index c57279d70ef..5e9ff4f1e80 100644 --- a/public/app/features/alerting/unified/RuleViewer.test.tsx +++ b/public/app/features/alerting/unified/RuleViewer.test.tsx @@ -15,7 +15,7 @@ import { CombinedRule } from 'app/types/unified-alerting'; import { RuleViewer } from './RuleViewer'; import { useCombinedRule } from './hooks/useCombinedRule'; import { useIsRuleEditable } from './hooks/useIsRuleEditable'; -import { getCloudRule, getGrafanaRule } from './mocks'; +import { getCloudRule, getGrafanaRule, grantUserPermissions } from './mocks'; const mockGrafanaRule = getGrafanaRule({ name: 'Test alert' }); const mockCloudRule = getCloudRule({ name: 'cloud test alert' }); @@ -61,6 +61,7 @@ const renderRuleViewer = () => { const ui = { actionButtons: { edit: byRole('link', { name: /edit/i }), + clone: byRole('link', { name: /clone/i }), delete: byRole('button', { name: /delete/i }), silence: byRole('link', { name: 'Silence' }), }, @@ -200,6 +201,36 @@ describe('RuleDetails RBAC', () => { // Assert expect(ui.actionButtons.silence.query()).toBeInTheDocument(); }); + + it('Should render clone button for users having create rule permission', async () => { + mocks.useIsRuleEditable.mockReturnValue({ loading: false, isEditable: false }); + mockCombinedRule.mockReturnValue({ + result: getGrafanaRule({ name: 'Grafana rule' }), + loading: false, + dispatched: true, + }); + grantUserPermissions([AccessControlAction.AlertingRuleCreate]); + + await renderRuleViewer(); + + expect(ui.actionButtons.clone.get()).toBeInTheDocument(); + }); + + it('Should NOT render clone button for users without create rule permission', async () => { + mocks.useIsRuleEditable.mockReturnValue({ loading: false, isEditable: true }); + mockCombinedRule.mockReturnValue({ + result: getGrafanaRule({ name: 'Grafana rule' }), + loading: false, + dispatched: true, + }); + + const { AlertingRuleRead, AlertingRuleUpdate, AlertingRuleDelete } = AccessControlAction; + grantUserPermissions([AlertingRuleRead, AlertingRuleUpdate, AlertingRuleDelete]); + + await renderRuleViewer(); + + expect(ui.actionButtons.clone.query()).not.toBeInTheDocument(); + }); }); describe('Cloud rules action buttons', () => { let mockCombinedRule: jest.MockedFn; diff --git a/public/app/features/alerting/unified/RuleViewer.tsx b/public/app/features/alerting/unified/RuleViewer.tsx index 3b493ed5f45..7ae268b8225 100644 --- a/public/app/features/alerting/unified/RuleViewer.tsx +++ b/public/app/features/alerting/unified/RuleViewer.tsx @@ -10,6 +10,7 @@ import { Button, Collapse, Icon, + IconButton, LoadingPlaceholder, useStyles2, VerticalGroup, @@ -18,7 +19,7 @@ import { import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { DEFAULT_PER_PAGE_PAGINATION } from '../../../core/constants'; -import { AlertQuery } from '../../../types/unified-alerting-dto'; +import { AlertQuery, GrafanaRuleDefinition } from '../../../types/unified-alerting-dto'; import { GrafanaRuleQueryViewer, QueryPreview } from './GrafanaRuleQueryViewer'; import { AlertLabels } from './components/AlertLabels'; @@ -181,7 +182,7 @@ export function RuleViewer({ match }: RuleViewerProps) { )} {!!rule.labels && !!Object.keys(rule.labels).length && ( - + )} @@ -190,7 +191,10 @@ export function RuleViewer({ match }: RuleViewerProps) {
{isFederatedRule && } - {`${rule.namespace.name} / ${rule.group.name}`} + + {rule.namespace.name} / {rule.group.name} + + {isGrafanaRulerRule(rule.rulerRule) && }
@@ -244,6 +248,17 @@ export function RuleViewer({ match }: RuleViewerProps) { ); } +function GrafanaRuleUID({ rule }: { rule: GrafanaRuleDefinition }) { + const styles = useStyles2(getStyles); + const copyUID = () => navigator.clipboard && navigator.clipboard.writeText(rule.uid); + + return ( + + {rule.uid} + + ); +} + function isLoading(data: Record): boolean { return !!Object.values(data).find((d) => d.state === LoadingState.Loading); } @@ -278,13 +293,26 @@ const getStyles = (theme: GrafanaTheme2) => { details: css` display: flex; flex-direction: row; + gap: ${theme.spacing(4)}; `, leftSide: css` flex: 1; `, rightSide: css` - padding-left: 90px; - width: 300px; + padding-right: ${theme.spacing(3)}; + `, + rightSideDetails: css` + & > div:first-child { + width: auto; + } + `, + labels: css` + justify-content: flex-start; + `, + ruleUid: css` + display: flex; + align-items: center; + gap: ${theme.spacing(1)}; `, }; }; diff --git a/public/app/features/alerting/unified/Silences.test.tsx b/public/app/features/alerting/unified/Silences.test.tsx index a6669e4c203..648f790a4f0 100644 --- a/public/app/features/alerting/unified/Silences.test.tsx +++ b/public/app/features/alerting/unified/Silences.test.tsx @@ -56,7 +56,7 @@ const ui = { silencesTable: byTestId('dynamic-table'), silenceRow: byTestId('row'), silencedAlertCell: byTestId('alerts'), - addSilenceButton: byRole('button', { name: /new silence/i }), + addSilenceButton: byRole('button', { name: /add silence/i }), queryBar: byPlaceholderText('Search'), editor: { timeRange: byLabelText('Timepicker', { exact: false }), diff --git a/public/app/features/alerting/unified/TODO.md b/public/app/features/alerting/unified/TODO.md index 52286bfaaf9..c9d508bb51c 100644 --- a/public/app/features/alerting/unified/TODO.md +++ b/public/app/features/alerting/unified/TODO.md @@ -17,6 +17,7 @@ If the item needs more rationale and you feel like a single sentence is inedequa ## Refactoring - Get rid of "+ Add new" in drop-downs : Let's see if is there a way we can make it work with ` + - {input.map((dataFrame) => ( -
-
-
- {dataFrame.refId} ({dataFrame.name}) -
- - - onToggleDataFrame(dataFrame)} - /> - - onResourceChange(event.currentTarget.value, index)} - placeholder="ex: /subscriptions/$subId" - /> - - ) : ( - <> - - - onResourceChange({ ...resource, subscription: event.currentTarget.value }, index) - } - placeholder="aaaaaaaa-bbbb-cccc-dddd-eeeeeeee" - /> - - - - onResourceChange({ ...resource, resourceGroup: event.currentTarget.value }, index) - } - placeholder="resource-group" - /> - - - - onResourceChange({ ...resource, metricNamespace: event.currentTarget.value }, index) - } - placeholder="Microsoft.Insights/metricNamespaces" - /> - - - - onResourceChange({ ...resource, resourceName: event.currentTarget.value }, index) - } - placeholder="name" - /> - - - )} -
- ))} - - -
- ); -}; - -export default Advanced; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/ResourcePicker.test.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/ResourcePicker.test.tsx index 3bad6927b27..6b4b330882e 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/ResourcePicker.test.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/ResourcePicker.test.tsx @@ -232,85 +232,6 @@ describe('AzureMonitor ResourcePicker', () => { expect(onApply).toBeCalledWith([]); }); - it('should call onApply with a new subscription uri when a user types it in the selection box', async () => { - const onApply = jest.fn(); - render(); - const subscriptionCheckbox = await screen.findByLabelText('Primary Subscription'); - expect(subscriptionCheckbox).toBeInTheDocument(); - expect(subscriptionCheckbox).not.toBeChecked(); - - const advancedSection = screen.getByText('Advanced'); - advancedSection.click(); - - const advancedInput = await screen.findByLabelText('Resource URI'); - await userEvent.type(advancedInput, '/subscriptions/def-123'); - - const applyButton = screen.getByRole('button', { name: 'Apply' }); - applyButton.click(); - - expect(onApply).toBeCalledTimes(1); - expect(onApply).toBeCalledWith(['/subscriptions/def-123']); - }); - - it('should call onApply with a new subscription when a user types it in the selection box', async () => { - const onApply = jest.fn(); - render(); - const subscriptionCheckbox = await screen.findByLabelText('Primary Subscription'); - expect(subscriptionCheckbox).toBeInTheDocument(); - expect(subscriptionCheckbox).not.toBeChecked(); - - const advancedSection = screen.getByText('Advanced'); - advancedSection.click(); - - const advancedInput = await screen.findByLabelText('Subscription'); - await userEvent.type(advancedInput, 'def-123'); - const nsInput = await screen.findByLabelText('Namespace'); - await userEvent.type(nsInput, 'ns'); - const rgInput = await screen.findByLabelText('Resource Group'); - await userEvent.type(rgInput, 'rg'); - const rnInput = await screen.findByLabelText('Resource Name'); - await userEvent.type(rnInput, 'rn'); - - const applyButton = screen.getByRole('button', { name: 'Apply' }); - applyButton.click(); - - expect(onApply).toBeCalledTimes(1); - expect(onApply).toBeCalledWith([ - { subscription: 'def-123', metricNamespace: 'ns', resourceGroup: 'rg', resourceName: 'rn' }, - ]); - }); - - it('should show unselect a subscription if the value is manually edited', async () => { - render( - - ); - const checkboxes = await screen.findAllByLabelText('web-server'); - expect(checkboxes.length).toBe(2); - expect(checkboxes[0]).toBeChecked(); - expect(checkboxes[1]).toBeChecked(); - - const advancedSection = screen.getByText('Advanced'); - advancedSection.click(); - - const advancedInput = await screen.findByLabelText('Subscription'); - await userEvent.type(advancedInput, 'def-123'); - - const updatedCheckboxes = await screen.findAllByLabelText('web-server'); - expect(updatedCheckboxes.length).toBe(1); - expect(updatedCheckboxes[0]).not.toBeChecked(); - }); - it('renders a search field which show search results when there are results', async () => { render(); const searchRow1 = screen.queryByLabelText('search-result'); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/ResourcePicker.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/ResourcePicker.tsx index c99550162c3..475215e0383 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/ResourcePicker.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/ResourcePicker.tsx @@ -2,7 +2,6 @@ import { cx } from '@emotion/css'; import React, { useCallback, useEffect, useState } from 'react'; import { useEffectOnce } from 'react-use'; -import { config } from '@grafana/runtime'; import { Alert, Button, LoadingPlaceholder, useStyles2 } from '@grafana/ui'; import { selectors } from '../../e2e/selectors'; @@ -11,7 +10,6 @@ import { AzureMetricResource } from '../../types'; import messageFromError from '../../utils/messageFromError'; import { Space } from '../Space'; -import Advanced from './Advanced'; import AdvancedMulti from './AdvancedMulti'; import NestedRow from './NestedRow'; import Search from './Search'; @@ -29,6 +27,7 @@ interface ResourcePickerProps { onCancel: () => void; disableRow: (row: ResourceRow, selectedRows: ResourceRowGroup) => boolean; renderAdvanced: (resources: T[], onChange: (resources: T[]) => void) => React.ReactNode; + selectionNotice?: (selectedRows: ResourceRowGroup) => string; } const ResourcePicker = ({ @@ -40,6 +39,7 @@ const ResourcePicker = ({ queryType, disableRow, renderAdvanced, + selectionNotice, }: ResourcePickerProps) => { const styles = useStyles2(getStyles); @@ -49,6 +49,7 @@ const ResourcePicker = ({ const [internalSelected, setInternalSelected] = useState(resources); const [errorMessage, setErrorMessage] = useState(undefined); const [shouldShowLimitFlag, setShouldShowLimitFlag] = useState(false); + const selectionNoticeText = selectionNotice?.(selectedRows); // Sync the resourceURI prop to internal state useEffect(() => { @@ -120,7 +121,7 @@ const ResourcePicker = ({ if (isSelected) { const newRes = queryType === 'logs' ? row.uri : parseMultipleResourceDetails([row.uri], row.location)[0]; const newSelected = internalSelected ? internalSelected.concat(newRes) : [newRes]; - setInternalSelected(newSelected); + setInternalSelected(newSelected.filter((r) => isValid(r))); } else { const newInternalSelected = internalSelected?.filter((r) => { return !matchURI(resourceToString(r), row.uri); @@ -241,18 +242,19 @@ const ResourcePicker = ({
+ {selectionNoticeText?.length ? ( + + {selectionNoticeText} + + ) : null} )} - {config.featureToggles.azureMultipleResourcePicker ? ( - setInternalSelected(r)} - renderAdvanced={renderAdvanced} - /> - ) : ( - setInternalSelected(r)} /> - )} + setInternalSelected(r)} + renderAdvanced={renderAdvanced} + /> diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/VariableEditor/VariableEditor.test.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/VariableEditor/VariableEditor.test.tsx index 319b03cd96e..a0f8bbcbfe3 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/VariableEditor/VariableEditor.test.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/VariableEditor/VariableEditor.test.tsx @@ -258,10 +258,12 @@ describe('VariableEditor:', () => { await waitFor(() => expect(screen.getByText('Logs')).toBeInTheDocument()); await selectAndRerender('select query type', 'Resource Names', onChange, rerender); await selectAndRerender('select subscription', 'Primary Subscription', onChange, rerender); + await selectAndRerender('select region', 'North Europe', onChange, rerender); expect(onChange).toHaveBeenCalledWith( expect.objectContaining({ queryType: AzureQueryType.ResourceNamesQuery, subscription: 'sub', + region: 'northeurope', refId: 'A', }) ); @@ -323,5 +325,21 @@ describe('VariableEditor:', () => { }) ); }); + + it('should run the query if requesting regions', async () => { + const onChange = jest.fn(); + const { rerender } = render(); + // wait for initial load + await waitFor(() => expect(screen.getByText('Logs')).toBeInTheDocument()); + await selectAndRerender('select query type', 'Regions', onChange, rerender); + await selectAndRerender('select subscription', 'Primary Subscription', onChange, rerender); + expect(onChange).toHaveBeenCalledWith( + expect.objectContaining({ + queryType: AzureQueryType.LocationsQuery, + subscription: 'sub', + refId: 'A', + }) + ); + }); }); }); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/VariableEditor/VariableEditor.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/VariableEditor/VariableEditor.tsx index 3b5feed71d0..c0716cb2ed9 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/VariableEditor/VariableEditor.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/VariableEditor/VariableEditor.tsx @@ -1,4 +1,4 @@ -import { get } from 'lodash'; +import { get, isEqual } from 'lodash'; import React, { useEffect, useState } from 'react'; import { useEffectOnce } from 'react-use'; @@ -30,6 +30,7 @@ const VariableEditor = (props: Props) => { { label: 'Subscriptions', value: AzureQueryType.SubscriptionsQuery }, { label: 'Resource Groups', value: AzureQueryType.ResourceGroupsQuery }, { label: 'Namespaces', value: AzureQueryType.NamespacesQuery }, + { label: 'Regions', value: AzureQueryType.LocationsQuery }, { label: 'Resource Names', value: AzureQueryType.ResourceNamesQuery }, { label: 'Metric Names', value: AzureQueryType.MetricNamesQuery }, { label: 'Workspaces', value: AzureQueryType.WorkspacesQuery }, @@ -50,6 +51,7 @@ const VariableEditor = (props: Props) => { const [requireSubscription, setRequireSubscription] = useState(false); const [hasResourceGroup, setHasResourceGroup] = useState(false); const [hasNamespace, setHasNamespace] = useState(false); + const [hasRegion, setHasRegion] = useState(false); const [requireResourceGroup, setRequireResourceGroup] = useState(false); const [requireNamespace, setRequireNamespace] = useState(false); const [requireResource, setRequireResource] = useState(false); @@ -57,12 +59,15 @@ const VariableEditor = (props: Props) => { const [resourceGroups, setResourceGroups] = useState([]); const [namespaces, setNamespaces] = useState([]); const [resources, setResources] = useState([]); + const [regions, setRegions] = useState([]); const [errorMessage, setError] = useLastError(); const queryType = typeof query === 'string' ? '' : query.queryType; useEffect(() => { migrateQuery(query, { datasource: datasource }).then((migratedQuery) => { - onChange(migratedQuery); + if (!isEqual(query, migratedQuery)) { + onChange(migratedQuery); + } }); }, [query, datasource, onChange]); @@ -86,6 +91,7 @@ const VariableEditor = (props: Props) => { setRequireSubscription(true); setHasResourceGroup(true); setHasNamespace(true); + setHasRegion(true); break; case AzureQueryType.MetricNamesQuery: setRequireSubscription(true); @@ -93,6 +99,9 @@ const VariableEditor = (props: Props) => { setRequireNamespace(true); setRequireResource(true); break; + case AzureQueryType.LocationsQuery: + setRequireSubscription(true); + break; } }, [queryType]); @@ -133,6 +142,16 @@ const VariableEditor = (props: Props) => { } }, [datasource, subscription, resourceGroup]); + useEffect(() => { + if (subscription) { + datasource.azureMonitorDatasource.getLocations([subscription]).then((rgs) => { + const regions: SelectableValue[] = []; + rgs.forEach((r) => regions.push({ label: r.displayName, value: r.name })); + setRegions(regions); + }); + } + }, [datasource, subscription, resourceGroup]); + const namespace = (typeof query === 'object' && query.namespace) || ''; useEffect(() => { if (subscription) { @@ -189,6 +208,13 @@ const VariableEditor = (props: Props) => { }); }; + const onChangeRegion = (selectableValue: SelectableValue) => { + onChange({ + ...query, + region: selectableValue.value, + }); + }; + const onChangeResource = (selectableValue: SelectableValue) => { onChange({ ...query, @@ -294,6 +320,22 @@ const VariableEditor = (props: Props) => { /> )} + {hasRegion && ( + + - - - - ); -}); + + + + {showMaxLines && ( + + + + )} + +