Merge remote-tracking branch 'origin/main' into zoltan/mssql-migration
This commit is contained in:
+17
-27
@@ -86,8 +86,8 @@ exports[`no enzyme tests`] = {
|
||||
"public/app/features/folders/FolderSettingsPage.test.tsx:1109052730": [
|
||||
[0, 19, 13, "RegExp match", "2409514259"]
|
||||
],
|
||||
"public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx:227258837": [
|
||||
[0, 19, 13, "RegExp match", "2409514259"]
|
||||
"public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx:4057721851": [
|
||||
[1, 19, 13, "RegExp match", "2409514259"]
|
||||
],
|
||||
"public/app/plugins/datasource/elasticsearch/configuration/ConfigEditor.test.tsx:3481855642": [
|
||||
[0, 26, 13, "RegExp match", "2409514259"]
|
||||
@@ -4771,9 +4771,6 @@ exports[`better eslint`] = {
|
||||
"public/app/features/datasources/DataSourceDashboards.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/features/datasources/DataSourcesListPage.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/features/datasources/__mocks__/dataSourcesMocks.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
@@ -5720,11 +5717,9 @@ exports[`better eslint`] = {
|
||||
"public/app/features/scenes/components/SceneFlexLayout.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/features/scenes/core/SceneComponentEditWrapper.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"]
|
||||
"public/app/features/scenes/core/SceneComponentWrapper.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
||||
],
|
||||
"public/app/features/scenes/core/SceneObjectBase.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
@@ -6050,9 +6045,6 @@ exports[`better eslint`] = {
|
||||
"public/app/features/transformers/utils.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/features/users/UsersActionBar.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/features/users/__mocks__/userMocks.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||
@@ -6655,8 +6647,12 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
|
||||
],
|
||||
"public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/plugins/datasource/cloudwatch/components/ConfigEditor.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"]
|
||||
],
|
||||
"public/app/plugins/datasource/cloudwatch/components/LogsQueryEditor.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
@@ -6664,7 +6660,8 @@ exports[`better eslint`] = {
|
||||
"public/app/plugins/datasource/cloudwatch/components/LogsQueryField.test.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.", "2"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"]
|
||||
],
|
||||
"public/app/plugins/datasource/cloudwatch/components/LogsQueryField.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
@@ -7564,10 +7561,7 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "13"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "14"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "15"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "16"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "17"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "18"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "19"]
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "16"]
|
||||
],
|
||||
"public/app/plugins/datasource/influxdb/specs/influx_query_model.test.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
@@ -9650,11 +9644,10 @@ exports[`better eslint`] = {
|
||||
"public/app/plugins/panel/histogram/Histogram.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "3"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "5"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "6"]
|
||||
[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"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "5"]
|
||||
],
|
||||
"public/app/plugins/panel/icon/IconPanel.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
@@ -10354,9 +10347,6 @@ exports[`no undocumented stories`] = {
|
||||
"packages/grafana-ui/src/components/QueryField/QueryField.story.tsx:5381": [
|
||||
[0, 0, 0, "No undocumented stories are allowed, please add an .mdx file with some documentation", "5381"]
|
||||
],
|
||||
"packages/grafana-ui/src/components/RefreshPicker/RefreshPicker.story.tsx:5381": [
|
||||
[0, 0, 0, "No undocumented stories are allowed, please add an .mdx file with some documentation", "5381"]
|
||||
],
|
||||
"packages/grafana-ui/src/components/SecretTextArea/SecretTextArea.story.tsx:5381": [
|
||||
[0, 0, 0, "No undocumented stories are allowed, please add an .mdx file with some documentation", "5381"]
|
||||
],
|
||||
|
||||
+2
-2
@@ -7,7 +7,7 @@
|
||||
load('scripts/drone/pipelines/pr.star', 'pr_pipelines')
|
||||
load('scripts/drone/pipelines/main.star', 'main_pipelines')
|
||||
load('scripts/drone/pipelines/docs.star', 'docs_pipelines')
|
||||
load('scripts/drone/pipelines/release.star', 'release_pipelines', 'publish_image_pipelines', 'publish_artifacts_pipelines', 'publish_npm_pipelines', 'publish_packages_pipeline')
|
||||
load('scripts/drone/pipelines/release.star', 'release_pipelines', 'publish_image_pipelines', 'publish_artifacts_pipelines', 'publish_npm_pipelines', 'publish_packages_pipeline', 'artifacts_page_pipeline')
|
||||
load('scripts/drone/version.star', 'version_branch_pipelines')
|
||||
load('scripts/drone/pipelines/cron.star', 'cronjobs')
|
||||
load('scripts/drone/vault.star', 'secrets')
|
||||
@@ -17,5 +17,5 @@ def main(ctx):
|
||||
return pr_pipelines(edition=edition) + main_pipelines(edition=edition) + release_pipelines() + \
|
||||
publish_image_pipelines('public') + publish_image_pipelines('security') + \
|
||||
publish_artifacts_pipelines('security') + publish_artifacts_pipelines('public') + \
|
||||
publish_npm_pipelines('public') + publish_packages_pipeline() + \
|
||||
publish_npm_pipelines('public') + publish_packages_pipeline() + artifacts_page_pipeline() + \
|
||||
version_branch_pipelines() + cronjobs(edition=edition) + secrets()
|
||||
|
||||
+153
-70
@@ -1,6 +1,49 @@
|
||||
---
|
||||
depends_on: []
|
||||
kind: pipeline
|
||||
name: pr-verify-drone
|
||||
node:
|
||||
type: no-parallel
|
||||
platform:
|
||||
arch: amd64
|
||||
os: linux
|
||||
services: []
|
||||
steps:
|
||||
- commands:
|
||||
- echo $DRONE_RUNNER_NAME
|
||||
image: alpine:3.15
|
||||
name: identify-runner
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
- commands:
|
||||
- ./bin/grabpl verify-drone
|
||||
depends_on:
|
||||
- grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: lint-drone
|
||||
trigger:
|
||||
event:
|
||||
- pull_request
|
||||
paths:
|
||||
exclude:
|
||||
- docs/**
|
||||
- '*.md'
|
||||
include:
|
||||
- scripts/drone/**
|
||||
- .drone.yml
|
||||
- .drone.star
|
||||
type: docker
|
||||
volumes:
|
||||
- host:
|
||||
path: /var/run/docker.sock
|
||||
name: docker
|
||||
---
|
||||
depends_on: []
|
||||
kind: pipeline
|
||||
name: pr-test-frontend
|
||||
node:
|
||||
type: no-parallel
|
||||
@@ -15,7 +58,7 @@ steps:
|
||||
name: identify-runner
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -91,7 +134,7 @@ steps:
|
||||
name: identify-runner
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -116,12 +159,6 @@ steps:
|
||||
- verify-gen-cue
|
||||
image: grafana/build-container:1.5.7
|
||||
name: wire-install
|
||||
- commands:
|
||||
- ./bin/grabpl verify-drone
|
||||
depends_on:
|
||||
- grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: lint-drone
|
||||
- commands:
|
||||
- |-
|
||||
echo -e "unknwon
|
||||
@@ -197,7 +234,7 @@ steps:
|
||||
name: identify-runner
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -243,7 +280,6 @@ steps:
|
||||
from_secret: drone_token
|
||||
- commands:
|
||||
- ./bin/grabpl build-backend --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER}
|
||||
--variants linux-amd64,linux-amd64-musl,darwin-amd64,windows-amd64
|
||||
depends_on:
|
||||
- gen-version
|
||||
- wire-install
|
||||
@@ -455,7 +491,7 @@ services:
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -542,7 +578,7 @@ services: []
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -632,7 +668,7 @@ services: []
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -721,7 +757,7 @@ steps:
|
||||
name: identify-runner
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -789,7 +825,7 @@ steps:
|
||||
name: identify-runner
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -884,7 +920,7 @@ steps:
|
||||
name: identify-runner
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -979,7 +1015,7 @@ steps:
|
||||
image: grafana/build-container:1.5.7
|
||||
name: build-frontend-packages
|
||||
- commands:
|
||||
- ./bin/grabpl build-plugins --jobs 8 --edition oss --sign --signing-admin
|
||||
- ./bin/grabpl build-plugins --jobs 8 --edition oss
|
||||
depends_on:
|
||||
- gen-version
|
||||
- yarn-install
|
||||
@@ -1230,7 +1266,7 @@ steps:
|
||||
repo:
|
||||
- grafana/grafana
|
||||
- commands:
|
||||
- ./bin/grabpl upload-packages --edition oss --packages-bucket grafana-downloads
|
||||
- ./bin/grabpl upload-packages --edition oss
|
||||
depends_on:
|
||||
- end-to-end-tests-dashboards-suite
|
||||
- end-to-end-tests-panels-suite
|
||||
@@ -1247,7 +1283,7 @@ steps:
|
||||
repo:
|
||||
- grafana/grafana
|
||||
- commands:
|
||||
- ./bin/grabpl upload-cdn --edition oss --src-bucket "grafana-static-assets"
|
||||
- ./bin/grabpl upload-cdn --edition oss
|
||||
depends_on:
|
||||
- grafana-server
|
||||
environment:
|
||||
@@ -1308,7 +1344,7 @@ services:
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -1398,7 +1434,7 @@ steps:
|
||||
name: identify-runner
|
||||
- commands:
|
||||
- $$ProgressPreference = "SilentlyContinue"
|
||||
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/windows/grabpl.exe
|
||||
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/windows/grabpl.exe
|
||||
-OutFile grabpl.exe
|
||||
image: grafana/ci-wix:0.1.1
|
||||
name: windows-init
|
||||
@@ -1486,10 +1522,16 @@ services: []
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
- commands:
|
||||
- ./bin/grabpl gen-version --build-id ${DRONE_BUILD_NUMBER}
|
||||
depends_on:
|
||||
- grabpl
|
||||
image: grafana/build-container:1.5.7
|
||||
name: gen-version
|
||||
- commands:
|
||||
- echo $DRONE_RUNNER_NAME
|
||||
image: alpine:3.15
|
||||
@@ -1498,7 +1540,7 @@ steps:
|
||||
- ./bin/grabpl store-packages --edition oss --gcp-key /tmp/gcpkey.json --build-id
|
||||
${DRONE_BUILD_NUMBER}
|
||||
depends_on:
|
||||
- grabpl
|
||||
- gen-version
|
||||
environment:
|
||||
GCP_KEY:
|
||||
from_secret: gcp_key
|
||||
@@ -1571,7 +1613,7 @@ steps:
|
||||
name: identify-runner
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -1628,7 +1670,7 @@ steps:
|
||||
image: grafana/build-container:1.5.7
|
||||
name: build-frontend-packages
|
||||
- commands:
|
||||
- ./bin/grabpl build-plugins --jobs 8 --edition oss --sign --signing-admin
|
||||
- ./bin/grabpl build-plugins --jobs 8 --edition oss
|
||||
depends_on:
|
||||
- gen-version
|
||||
- yarn-install
|
||||
@@ -1778,8 +1820,7 @@ steps:
|
||||
image: grafana/build-container:1.5.7
|
||||
name: build-storybook
|
||||
- commands:
|
||||
- ./bin/grabpl upload-cdn --edition oss --src-bucket "$${PRERELEASE_BUCKET}" --src-dir
|
||||
artifacts/static-assets
|
||||
- ./bin/grabpl upload-cdn --edition oss
|
||||
depends_on:
|
||||
- grafana-server
|
||||
environment:
|
||||
@@ -1790,7 +1831,7 @@ steps:
|
||||
image: grafana/grafana-ci-deploy:1.3.1
|
||||
name: upload-cdn-assets
|
||||
- commands:
|
||||
- ./bin/grabpl upload-packages --edition oss --packages-bucket $${PRERELEASE_BUCKET}/artifacts/downloads
|
||||
- ./bin/grabpl upload-packages --edition oss
|
||||
depends_on:
|
||||
- end-to-end-tests-dashboards-suite
|
||||
- end-to-end-tests-panels-suite
|
||||
@@ -1869,7 +1910,7 @@ steps:
|
||||
name: identify-runner
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -2004,7 +2045,7 @@ services:
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -2098,7 +2139,7 @@ steps:
|
||||
name: identify-runner
|
||||
- commands:
|
||||
- $$ProgressPreference = "SilentlyContinue"
|
||||
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/windows/grabpl.exe
|
||||
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/windows/grabpl.exe
|
||||
-OutFile grabpl.exe
|
||||
image: grafana/ci-wix:0.1.1
|
||||
name: windows-init
|
||||
@@ -2157,7 +2198,7 @@ services: []
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -2244,7 +2285,7 @@ steps:
|
||||
image: grafana/build-container:1.5.7
|
||||
name: build-frontend-packages
|
||||
- commands:
|
||||
- ./bin/grabpl build-plugins --jobs 8 --edition enterprise --sign --signing-admin
|
||||
- ./bin/grabpl build-plugins --jobs 8 --edition enterprise
|
||||
depends_on:
|
||||
- gen-version
|
||||
- yarn-install
|
||||
@@ -2393,8 +2434,7 @@ steps:
|
||||
- success
|
||||
- failure
|
||||
- commands:
|
||||
- ./bin/grabpl upload-cdn --edition enterprise --src-bucket "$${PRERELEASE_BUCKET}"
|
||||
--src-dir artifacts/static-assets
|
||||
- ./bin/grabpl upload-cdn --edition enterprise
|
||||
depends_on:
|
||||
- package
|
||||
environment:
|
||||
@@ -2405,7 +2445,7 @@ steps:
|
||||
image: grafana/grafana-ci-deploy:1.3.1
|
||||
name: upload-cdn-assets
|
||||
- commands:
|
||||
- ./bin/grabpl upload-packages --edition enterprise --packages-bucket $${PRERELEASE_BUCKET}/artifacts/downloads
|
||||
- ./bin/grabpl upload-packages --edition enterprise
|
||||
depends_on:
|
||||
- package
|
||||
environment:
|
||||
@@ -2446,8 +2486,7 @@ steps:
|
||||
image: grafana/build-container:1.5.7
|
||||
name: package-enterprise2
|
||||
- commands:
|
||||
- ./bin/grabpl upload-cdn --edition enterprise2 --src-bucket "$${PRERELEASE_BUCKET}"
|
||||
--src-dir artifacts/static-assets
|
||||
- ./bin/grabpl upload-cdn --edition enterprise2
|
||||
depends_on:
|
||||
- package-enterprise2
|
||||
environment:
|
||||
@@ -2458,7 +2497,7 @@ steps:
|
||||
image: grafana/grafana-ci-deploy:1.3.1
|
||||
name: upload-cdn-assets-enterprise2
|
||||
- commands:
|
||||
- ./bin/grabpl upload-packages --edition enterprise2 --packages-bucket $${PRERELEASE_BUCKET}/artifacts/downloads-enterprise2
|
||||
- ./bin/grabpl upload-packages --edition enterprise2
|
||||
depends_on:
|
||||
- package-enterprise2
|
||||
environment:
|
||||
@@ -2505,7 +2544,7 @@ services: []
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -2688,7 +2727,7 @@ services:
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -2830,7 +2869,7 @@ steps:
|
||||
name: identify-runner
|
||||
- commands:
|
||||
- $$ProgressPreference = "SilentlyContinue"
|
||||
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/windows/grabpl.exe
|
||||
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/windows/grabpl.exe
|
||||
-OutFile grabpl.exe
|
||||
- git clone "https://$$env:GITHUB_TOKEN@github.com/grafana/grafana-enterprise.git"
|
||||
- cd grafana-enterprise
|
||||
@@ -2905,7 +2944,7 @@ services: []
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -2983,7 +3022,7 @@ services: []
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -3044,7 +3083,7 @@ services: []
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -3123,7 +3162,7 @@ services: []
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -3185,7 +3224,7 @@ services: []
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -3223,7 +3262,7 @@ services: []
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -3261,7 +3300,7 @@ services: []
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -3317,15 +3356,21 @@ services: []
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
- commands:
|
||||
- ./bin/grabpl gen-version ${DRONE_TAG}
|
||||
depends_on:
|
||||
- grabpl
|
||||
image: grafana/build-container:1.5.7
|
||||
name: gen-version
|
||||
- commands:
|
||||
- ./bin/grabpl store-packages --edition oss --packages-bucket grafana-downloads
|
||||
--gcp-key /tmp/gcpkey.json ${DRONE_TAG}
|
||||
depends_on:
|
||||
- grabpl
|
||||
- gen-version
|
||||
environment:
|
||||
GCP_KEY:
|
||||
from_secret: gcp_key
|
||||
@@ -3365,15 +3410,21 @@ services: []
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
- commands:
|
||||
- ./bin/grabpl gen-version ${DRONE_TAG}
|
||||
depends_on:
|
||||
- grabpl
|
||||
image: grafana/build-container:1.5.7
|
||||
name: gen-version
|
||||
- commands:
|
||||
- ./bin/grabpl store-packages --edition enterprise --packages-bucket grafana-downloads
|
||||
--gcp-key /tmp/gcpkey.json ${DRONE_TAG}
|
||||
depends_on:
|
||||
- grabpl
|
||||
- gen-version
|
||||
environment:
|
||||
GCP_KEY:
|
||||
from_secret: gcp_key
|
||||
@@ -3400,6 +3451,38 @@ volumes:
|
||||
---
|
||||
depends_on: []
|
||||
kind: pipeline
|
||||
name: publish-artifacts-page
|
||||
node:
|
||||
type: no-parallel
|
||||
platform:
|
||||
arch: amd64
|
||||
os: linux
|
||||
services: []
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
- commands:
|
||||
- ./bin/grabpl artifacts-page
|
||||
depends_on:
|
||||
- grabpl
|
||||
image: grafana/build-container:1.5.7
|
||||
name: artifacts-page
|
||||
trigger:
|
||||
event:
|
||||
- promote
|
||||
target: security
|
||||
type: docker
|
||||
volumes:
|
||||
- host:
|
||||
path: /var/run/docker.sock
|
||||
name: docker
|
||||
---
|
||||
depends_on: []
|
||||
kind: pipeline
|
||||
name: release-branch-oss-build-e2e-publish
|
||||
node:
|
||||
type: no-parallel
|
||||
@@ -3414,7 +3497,7 @@ steps:
|
||||
name: identify-runner
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -3471,7 +3554,7 @@ steps:
|
||||
image: grafana/build-container:1.5.7
|
||||
name: build-frontend-packages
|
||||
- commands:
|
||||
- ./bin/grabpl build-plugins --jobs 8 --edition oss --sign --signing-admin
|
||||
- ./bin/grabpl build-plugins --jobs 8 --edition oss
|
||||
depends_on:
|
||||
- gen-version
|
||||
- yarn-install
|
||||
@@ -3621,7 +3704,7 @@ steps:
|
||||
image: grafana/build-container:1.5.7
|
||||
name: build-storybook
|
||||
- commands:
|
||||
- ./bin/grabpl upload-cdn --edition oss --src-bucket "grafana-static-assets"
|
||||
- ./bin/grabpl upload-cdn --edition oss
|
||||
depends_on:
|
||||
- grafana-server
|
||||
environment:
|
||||
@@ -3635,7 +3718,7 @@ steps:
|
||||
repo:
|
||||
- grafana/grafana
|
||||
- commands:
|
||||
- ./bin/grabpl upload-packages --edition oss --packages-bucket grafana-downloads
|
||||
- ./bin/grabpl upload-packages --edition oss
|
||||
depends_on:
|
||||
- end-to-end-tests-dashboards-suite
|
||||
- end-to-end-tests-panels-suite
|
||||
@@ -3682,7 +3765,7 @@ steps:
|
||||
name: identify-runner
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -3811,7 +3894,7 @@ services:
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -3899,7 +3982,7 @@ steps:
|
||||
name: identify-runner
|
||||
- commands:
|
||||
- $$ProgressPreference = "SilentlyContinue"
|
||||
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/windows/grabpl.exe
|
||||
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/windows/grabpl.exe
|
||||
-OutFile grabpl.exe
|
||||
image: grafana/ci-wix:0.1.1
|
||||
name: windows-init
|
||||
@@ -3947,7 +4030,7 @@ services: []
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -4032,7 +4115,7 @@ steps:
|
||||
image: grafana/build-container:1.5.7
|
||||
name: build-frontend-packages
|
||||
- commands:
|
||||
- ./bin/grabpl build-plugins --jobs 8 --edition enterprise --sign --signing-admin
|
||||
- ./bin/grabpl build-plugins --jobs 8 --edition enterprise
|
||||
depends_on:
|
||||
- gen-version
|
||||
- yarn-install
|
||||
@@ -4193,7 +4276,7 @@ steps:
|
||||
image: grafana/build-container:1.5.7
|
||||
name: build-storybook
|
||||
- commands:
|
||||
- ./bin/grabpl upload-cdn --edition enterprise --src-bucket "grafana-static-assets"
|
||||
- ./bin/grabpl upload-cdn --edition enterprise
|
||||
depends_on:
|
||||
- package
|
||||
environment:
|
||||
@@ -4207,7 +4290,7 @@ steps:
|
||||
repo:
|
||||
- grafana/grafana
|
||||
- commands:
|
||||
- ./bin/grabpl upload-packages --edition enterprise --packages-bucket grafana-downloads
|
||||
- ./bin/grabpl upload-packages --edition enterprise
|
||||
depends_on:
|
||||
- package
|
||||
environment:
|
||||
@@ -4241,7 +4324,7 @@ steps:
|
||||
image: grafana/build-container:1.5.7
|
||||
name: package-enterprise2
|
||||
- commands:
|
||||
- ./bin/grabpl upload-cdn --edition enterprise2 --src-bucket "grafana-static-assets"
|
||||
- ./bin/grabpl upload-cdn --edition enterprise2
|
||||
depends_on:
|
||||
- package-enterprise2
|
||||
environment:
|
||||
@@ -4252,7 +4335,7 @@ steps:
|
||||
image: grafana/grafana-ci-deploy:1.3.1
|
||||
name: upload-cdn-assets-enterprise2
|
||||
- commands:
|
||||
- ./bin/grabpl upload-packages --edition enterprise2 --packages-bucket grafana-downloads-enterprise2
|
||||
- ./bin/grabpl upload-packages --edition enterprise2
|
||||
depends_on:
|
||||
- package-enterprise2
|
||||
environment:
|
||||
@@ -4293,7 +4376,7 @@ services: []
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -4467,7 +4550,7 @@ services:
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -4600,7 +4683,7 @@ steps:
|
||||
name: identify-runner
|
||||
- commands:
|
||||
- $$ProgressPreference = "SilentlyContinue"
|
||||
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.50/windows/grabpl.exe
|
||||
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.52/windows/grabpl.exe
|
||||
-OutFile grabpl.exe
|
||||
- git clone "https://$$env:GITHUB_TOKEN@github.com/grafana/grafana-enterprise.git"
|
||||
- cd grafana-enterprise
|
||||
@@ -4797,6 +4880,6 @@ kind: secret
|
||||
name: gcp_upload_artifacts_key
|
||||
---
|
||||
kind: signature
|
||||
hmac: 12fbd61337ed1ae006e8c0ff71d1290e27b5910211369cdd47e1d604afd9befa
|
||||
hmac: 55383abbbc205824d35aa689a0e00f374e74520d77dc387e354b063e4ade0869
|
||||
|
||||
...
|
||||
|
||||
@@ -144,6 +144,7 @@ lerna.json @grafana/frontend-ops
|
||||
.eslintrc @grafana/frontend-ops
|
||||
.pa11yci.conf.js @grafana/user-essentials
|
||||
.pa11yci-pr.conf.js @grafana/user-essentials
|
||||
.betterer.results @joshhunt
|
||||
|
||||
# @grafana/ui component documentation
|
||||
*.mdx @marcusolsson @jessover9000 @grafana/plugins-platform-frontend
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
"name":"datasource/Azure",
|
||||
"action":"addToProject",
|
||||
"addToProject":{
|
||||
"url":"https://github.com/orgs/grafana/projects/97"
|
||||
"url":"https://github.com/orgs/grafana/projects/190"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -103,7 +103,7 @@
|
||||
"name":"datasource/GoogleCloudMonitoring",
|
||||
"action":"addToProject",
|
||||
"addToProject":{
|
||||
"url":"https://github.com/orgs/grafana/projects/97"
|
||||
"url":"https://github.com/orgs/grafana/projects/190"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
+3
-2
@@ -576,7 +576,7 @@ tls_client_cert =
|
||||
tls_client_key =
|
||||
tls_client_ca =
|
||||
use_pkce = false
|
||||
auth_style =
|
||||
auth_style =
|
||||
|
||||
#################################### Basic Auth ##########################
|
||||
[auth.basic]
|
||||
@@ -597,6 +597,7 @@ enable_login_token = false
|
||||
#################################### Auth JWT ##########################
|
||||
[auth.jwt]
|
||||
enabled = false
|
||||
enable_login_token = false
|
||||
header_name =
|
||||
email_claim =
|
||||
username_claim =
|
||||
@@ -762,7 +763,7 @@ instrumentations_console_enabled = false
|
||||
instrumentations_webvitals_enabled = false
|
||||
|
||||
# Api Key, only applies to Grafana Javascript Agent provider
|
||||
api_key =
|
||||
api_key =
|
||||
|
||||
#################################### Usage Quotas ########################
|
||||
[quota]
|
||||
|
||||
@@ -25,6 +25,9 @@ bind_dn = "cn=admin,dc=grafana,dc=org"
|
||||
# If the password contains # or ; you have to wrap it with triple quotes. Ex """#password;"""
|
||||
bind_password = 'grafana'
|
||||
|
||||
# Timeout in seconds (applies to each host specified in the 'host' entry (space separated))
|
||||
timeout = 10
|
||||
|
||||
# User search filter, for example "(cn=%s)" or "(sAMAccountName=%s)" or "(uid=%s)"
|
||||
search_filter = "(cn=%s)"
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,9 @@
|
||||
#/bin/sh
|
||||
|
||||
VERSION=12.0.1 # set version here
|
||||
|
||||
cd /tmp
|
||||
git clone git@github.com:keycloak/keycloak-containers.git
|
||||
cd keycloak-containers/server
|
||||
git checkout $VERSION
|
||||
docker build -t "quay.io/keycloak/keycloak:${VERSION}" .
|
||||
@@ -0,0 +1,54 @@
|
||||
oauthkeycloakdb:
|
||||
image: postgres:12.2
|
||||
container_name: oauthkeycloakdb
|
||||
environment:
|
||||
POSTGRES_DB: keycloak
|
||||
POSTGRES_USER: keycloak
|
||||
POSTGRES_PASSWORD: password
|
||||
volumes:
|
||||
- ./docker/blocks/jwt_proxy/cloak.sql:/docker-entrypoint-initdb.d/cloak.sql
|
||||
restart: unless-stopped
|
||||
|
||||
oauthkeycloak:
|
||||
image: quay.io/keycloak/keycloak:12.0.1
|
||||
container_name: oauthkeycloak
|
||||
environment:
|
||||
DB_VENDOR: POSTGRES
|
||||
DB_ADDR: oauthkeycloakdb
|
||||
DB_DATABASE: keycloak
|
||||
DB_USER: keycloak
|
||||
DB_PASSWORD: password
|
||||
KEYCLOAK_USER: admin
|
||||
KEYCLOAK_PASSWORD: admin
|
||||
PROXY_ADDRESS_FORWARDING: "true"
|
||||
ports:
|
||||
- 8087:8080
|
||||
depends_on:
|
||||
- oauthkeycloakdb
|
||||
links:
|
||||
- "oauthkeycloakdb:oauthkeycloakdb"
|
||||
restart: unless-stopped
|
||||
|
||||
oauthproxy:
|
||||
image: docker.io/bitnami/oauth2-proxy:7.3.0
|
||||
container_name: oauthproxy
|
||||
command: [
|
||||
"--cookie-secret=yI-CWT5s4sBR2Zd0DDJJlTYc0aQ3jwGH15jYA18ZAQA=",
|
||||
"--upstream=http://localhost:3000",
|
||||
"--provider=keycloak",
|
||||
"--client-id=grafana-oauth",
|
||||
"--client-secret=d17b9ea9-bcb1-43d2-b132-d339e55872a8",
|
||||
"--login-url=http://127.0.0.1:8087/auth/realms/grafana/protocol/openid-connect/auth",
|
||||
"--redeem-url=http://127.0.0.1:8087/auth/realms/grafana/protocol/openid-connect/token",
|
||||
"--profile-url=http://127.0.0.1:8087/auth/realms/grafana/protocol/openid-connect/userinfo",
|
||||
"--validate-url=http://127.0.0.1:8087/auth/realms/grafana/protocol/openid-connect/userinfo",
|
||||
"--cookie-secure=false",
|
||||
"--http-address=0.0.0.0:8088",
|
||||
"--redirect-url=http://127.0.0.1:8088/oauth2/callback",
|
||||
"--pass-access-token=true",
|
||||
"--email-domain=*",
|
||||
]
|
||||
network_mode: "host"
|
||||
depends_on:
|
||||
- oauthkeycloak
|
||||
restart: unless-stopped
|
||||
@@ -0,0 +1 @@
|
||||
{"keys":[{"kid":"On2FQuJ8Y-909uJGWQEDkbzG-GRNmMc43HslEgVv_VQ","kty":"RSA","alg":"RS256","use":"sig","n":"qDmQHfTcOQOzmNJbVvtvuS8p_EgmiscP7vA_PZNyKx9O7utyGuoAmJH8e2w8gLIDDWHl5_x8aAIl_-TTPTSiyX8I68ryIdR28ZSe5u4pRdpXCVvJpOefKNIxQCTH7rs4KuRj0HZ2u1mu1Vz5_CeCCoKwKSmheD3u1xTJ8-VxQmdqfGxhuKtnkof7977HWOWy4GLDFqxyYHgihP_MmSeTmXUhVeZI6IOCqHMpF8eFWVGKM6V8rIKf8QO2K_vDJBM_3C933vMY8mqSQXbI3G54x-0myAaQXr4JkxjvUGKg5YC3ZXw7AjfZv_W_fQOG0GYp2hQ0akR4KNKT3XPNmpMVlQ","e":"AQAB","x5c":["MIICnTCCAYUCBgF+u1ir8jANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdncmFmYW5hMB4XDTIyMDIwMjE2NDkxN1oXDTMyMDIwMjE2NTA1N1owEjEQMA4GA1UEAwwHZ3JhZmFuYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKg5kB303DkDs5jSW1b7b7kvKfxIJorHD+7wPz2TcisfTu7rchrqAJiR/HtsPICyAw1h5ef8fGgCJf/k0z00osl/COvK8iHUdvGUnubuKUXaVwlbyaTnnyjSMUAkx+67OCrkY9B2drtZrtVc+fwnggqCsCkpoXg97tcUyfPlcUJnanxsYbirZ5KH+/e+x1jlsuBiwxascmB4IoT/zJknk5l1IVXmSOiDgqhzKRfHhVlRijOlfKyCn/EDtiv7wyQTP9wvd97zGPJqkkF2yNxueMftJsgGkF6+CZMY71BioOWAt2V8OwI32b/1v30DhtBmKdoUNGpEeCjSk91zzZqTFZUCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEABlW64QxuREB81VMGsyhj4Q5RykFaVuD5O8YlwUpmVfAVLzb0Drf54Kn4bnpnckKyYV+T+HsN4QXt81UE41xH0Aai2H3vrGH+PJf6aLPCDE+jpMqtN3n6IgImJXJPL8upMfhhWDv4nkM4uynEwWupzmrKi4oJuTETSMktJby4o6//XWnCzCVMoAGFJU4gtjBUzOMLW26zD+yc+BuUtfR3HzItVHSZKQSNSFO0kVS68RgrER8qJw07z3BOJ2bPpPM0PYyEngGMaowz/T6lI32ymGMWYMAnslthS1KAW9xcTBwnrW1nMhe5a0LPxIktys/wJtxIHZLc5sOddGT4xYklLg=="],"x5t":"prs-h1NBqOSJMH-tQWLTqguWets","x5t#S256":"YjK3HobZW8xbNL1IPDgFhCM41UC5c0hG2cxaF6v961Q"}]}
|
||||
@@ -0,0 +1,66 @@
|
||||
# OAUTH BLOCK
|
||||
## Devenv setup jwt auth
|
||||
|
||||
To launch the block, use the oauth source. Ex:
|
||||
|
||||
```bash
|
||||
make devenv sources="jwt_proxy"
|
||||
```
|
||||
|
||||
Here is the conf you need to add to your configuration file (conf/custom.ini):
|
||||
|
||||
```ini
|
||||
[auth]
|
||||
signout_redirect_url = http://127.0.0.1:8088/oauth2/sign_out
|
||||
|
||||
[auth.jwt]
|
||||
enabled = true
|
||||
enable_login_token = true
|
||||
header_name = X-Forwarded-Access-Token
|
||||
username_claim = login
|
||||
email_claim = email
|
||||
jwk_set_file = devenv/docker/blocks/oauth/jwks.json
|
||||
cache_ttl = 60m
|
||||
expected_claims = {"iss": "http://localhost:8087/auth/realms/grafana", "azp": "grafana-oauth"}
|
||||
auto_sign_up = true
|
||||
```
|
||||
|
||||
Access Grafana through:
|
||||
|
||||
```sh
|
||||
http://127.0.0.1:8088
|
||||
```
|
||||
|
||||
## Backing up keycloak DB
|
||||
|
||||
In case you want to make changes to the devenv setup, you can dump keycloack's DB:
|
||||
|
||||
```bash
|
||||
cd devenv;
|
||||
docker-compose exec -T oauthkeycloakdb bash -c "pg_dump -U keycloak keycloak" > docker/blocks/oauth/cloak.sql
|
||||
```
|
||||
|
||||
## Connecting to keycloack:
|
||||
|
||||
- keycloak admin: http://localhost:8087
|
||||
- keycloak admin login: admin:admin
|
||||
- grafana jwt viewer login: jwt-viewer:grafana
|
||||
- grafana jwt editor login: jwt-editor:grafana
|
||||
- grafana jwt admin login: jwt-admin:grafana
|
||||
|
||||
# Troubleshooting
|
||||
|
||||
## Mac M1 Users
|
||||
|
||||
The new arm64 architecture does not build for the latest docker image of keycloack. Refer to https://github.com/docker/for-mac/issues/5310 for the issue to see if it resolved.
|
||||
Until then you need to build the docker image locally and then run `devenv`.
|
||||
|
||||
1. Remove any lingering keycloack image
|
||||
```sh
|
||||
$ docker rmi $(docker images | grep 'keycloack')
|
||||
```
|
||||
1. Build keycloack image locally
|
||||
```sh
|
||||
$ ./docker-build-keycloack-m1-image.sh
|
||||
```
|
||||
1. Start from beginning of this readme
|
||||
@@ -0,0 +1 @@
|
||||
{"keys":[{"kid":"On2FQuJ8Y-909uJGWQEDkbzG-GRNmMc43HslEgVv_VQ","kty":"RSA","alg":"RS256","use":"sig","n":"qDmQHfTcOQOzmNJbVvtvuS8p_EgmiscP7vA_PZNyKx9O7utyGuoAmJH8e2w8gLIDDWHl5_x8aAIl_-TTPTSiyX8I68ryIdR28ZSe5u4pRdpXCVvJpOefKNIxQCTH7rs4KuRj0HZ2u1mu1Vz5_CeCCoKwKSmheD3u1xTJ8-VxQmdqfGxhuKtnkof7977HWOWy4GLDFqxyYHgihP_MmSeTmXUhVeZI6IOCqHMpF8eFWVGKM6V8rIKf8QO2K_vDJBM_3C933vMY8mqSQXbI3G54x-0myAaQXr4JkxjvUGKg5YC3ZXw7AjfZv_W_fQOG0GYp2hQ0akR4KNKT3XPNmpMVlQ","e":"AQAB","x5c":["MIICnTCCAYUCBgF+u1ir8jANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdncmFmYW5hMB4XDTIyMDIwMjE2NDkxN1oXDTMyMDIwMjE2NTA1N1owEjEQMA4GA1UEAwwHZ3JhZmFuYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKg5kB303DkDs5jSW1b7b7kvKfxIJorHD+7wPz2TcisfTu7rchrqAJiR/HtsPICyAw1h5ef8fGgCJf/k0z00osl/COvK8iHUdvGUnubuKUXaVwlbyaTnnyjSMUAkx+67OCrkY9B2drtZrtVc+fwnggqCsCkpoXg97tcUyfPlcUJnanxsYbirZ5KH+/e+x1jlsuBiwxascmB4IoT/zJknk5l1IVXmSOiDgqhzKRfHhVlRijOlfKyCn/EDtiv7wyQTP9wvd97zGPJqkkF2yNxueMftJsgGkF6+CZMY71BioOWAt2V8OwI32b/1v30DhtBmKdoUNGpEeCjSk91zzZqTFZUCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEABlW64QxuREB81VMGsyhj4Q5RykFaVuD5O8YlwUpmVfAVLzb0Drf54Kn4bnpnckKyYV+T+HsN4QXt81UE41xH0Aai2H3vrGH+PJf6aLPCDE+jpMqtN3n6IgImJXJPL8upMfhhWDv4nkM4uynEwWupzmrKi4oJuTETSMktJby4o6//XWnCzCVMoAGFJU4gtjBUzOMLW26zD+yc+BuUtfR3HzItVHSZKQSNSFO0kVS68RgrER8qJw07z3BOJ2bPpPM0PYyEngGMaowz/T6lI32ymGMWYMAnslthS1KAW9xcTBwnrW1nMhe5a0LPxIktys/wJtxIHZLc5sOddGT4xYklLg=="],"x5t":"prs-h1NBqOSJMH-tQWLTqguWets","x5t#S256":"YjK3HobZW8xbNL1IPDgFhCM41UC5c0hG2cxaF6v961Q"}]}
|
||||
@@ -1,6 +1,6 @@
|
||||
# OAUTH BLOCK
|
||||
|
||||
## Devenv setup
|
||||
## Devenv setup oauth
|
||||
|
||||
To launch the block, use the oauth source. Ex:
|
||||
```bash
|
||||
@@ -29,6 +29,76 @@ api_url = http://localhost:8087/auth/realms/grafana/protocol/openid-connect/user
|
||||
role_attribute_path = contains(roles[*], 'admin') && 'Admin' || contains(roles[*], 'editor') && 'Editor' || 'Viewer'
|
||||
```
|
||||
|
||||
## Devenv setup jwt auth
|
||||
|
||||
To launch the block, use the oauth source. Ex:
|
||||
|
||||
```bash
|
||||
make devenv sources="oauth"
|
||||
```
|
||||
|
||||
Here is the conf you need to add to your configuration file (conf/custom.ini):
|
||||
|
||||
```ini
|
||||
[auth.jwt]
|
||||
enabled = true
|
||||
header_name = X-JWT-Assertion
|
||||
username_claim = login
|
||||
email_claim = email
|
||||
jwk_set_file = devenv/docker/blocks/oauth/jwks.json
|
||||
cache_ttl = 60m
|
||||
expected_claims = {"iss": "http://localhost:8087/auth/realms/grafana", "azp": "grafana-oauth"}
|
||||
auto_sign_up = true
|
||||
```
|
||||
|
||||
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 \
|
||||
--header 'Content-Type: application/x-www-form-urlencoded' \
|
||||
--data client_id=grafana-oauth \
|
||||
--data grant_type=password \
|
||||
--data client_secret=d17b9ea9-bcb1-43d2-b132-d339e55872a8 \
|
||||
--data scope=openid \
|
||||
--data username=oauth-admin \
|
||||
--data password=grafana
|
||||
```
|
||||
|
||||
|
||||
Grafana call example:
|
||||
|
||||
```sh
|
||||
curl --request GET \
|
||||
--url http://127.0.0.1:3000/api/folders \
|
||||
--header 'Accept: application/json' \
|
||||
--header 'X-JWT-Assertion: eyJ......'
|
||||
```
|
||||
|
||||
### Alternative devenv setup jwk_set_url
|
||||
|
||||
Run a reverse proxy pointing to the jwk_set_url (only an https-uri can be used as jwk_set_url).
|
||||
|
||||
Ex (using localtunnel):
|
||||
|
||||
```sh
|
||||
npx localtunnel --port 8087
|
||||
```
|
||||
|
||||
And using the following conf:
|
||||
|
||||
```ini
|
||||
[auth.jwt]
|
||||
enabled = true
|
||||
header_name = X-JWT-Assertion
|
||||
username_claim = login
|
||||
email_claim = email
|
||||
jwk_set_url = <YOUR REVERSE PROXY URL>/auth/realms/grafana/protocol/openid-connect/certs
|
||||
cache_ttl = 60m
|
||||
expected_claims = {"iss": "http://localhost:8087/auth/realms/grafana", "azp": "grafana-oauth"}
|
||||
auto_sign_up = true
|
||||
```
|
||||
|
||||
## Backing up keycloak DB
|
||||
|
||||
In case you want to make changes to the devenv setup, you can dump keycloack's DB:
|
||||
@@ -62,4 +132,3 @@ $ docker rmi $(docker images | grep 'keycloack')
|
||||
$ ./docker-build-keycloack-m1-image.sh
|
||||
```
|
||||
1. Start from beginning of this readme
|
||||
|
||||
|
||||
@@ -14,24 +14,18 @@ weight: 700
|
||||
|
||||
# API keys
|
||||
|
||||
API keys can be used to interact with Grafana HTTP APIs.
|
||||
|
||||
We recommend using service accounts instead of API keys if you are on Grafana 8.5+, for more information refer to [About service accounts]({{< relref "../service-accounts/about-service-accounts/#" >}}).
|
||||
|
||||
{{< section >}}
|
||||
|
||||
## About API keys
|
||||
|
||||
An API key is a randomly generated string that external systems use to interact with Grafana HTTP APIs.
|
||||
|
||||
When you create an API key, you specify a **Role** that determines the permissions associated with the API key. Role permissions control that actions the API key can perform on Grafana resources. For more information about creating API keys, refer to [Create an API key]({{< relref "create-api-key/#" >}}).
|
||||
When you create an API key, you specify a **Role** that determines the permissions associated with the API key. Role permissions control that actions the API key can perform on Grafana resources.
|
||||
|
||||
> **Note:** If you use Grafana v8.5 or newer, use service accounts instead of API keys. For more information, refer to [Service accounts]({{< relref "../service-accounts/" >}}).
|
||||
|
||||
{{< section >}}
|
||||
|
||||
## Create an API key
|
||||
|
||||
Create an API key when you want to manage your computed workload with a user.
|
||||
|
||||
For more information about API keys, refer to [About API keys in Grafana]({{< relref "about-api-keys/" >}}).
|
||||
|
||||
This topic shows you how to create an API key using the Grafana UI. You can also create an API key using the Grafana HTTP API. For more information about creating API keys via the API, refer to [Create API key via API]({{< relref "../../developers/http_api/create-api-tokens-for-org/#how-to-create-a-new-organization-and-an-api-token" >}}).
|
||||
|
||||
### Before you begin:
|
||||
|
||||
@@ -29,7 +29,7 @@ If the user is aware of the change and intended it, then that's great! But if th
|
||||
|
||||
In Grafana, you can change your names and emails associated with groups or accounts in the Settings or Preferences. This topic provides instructions for each task.
|
||||
|
||||
{{< docs/shared "preferences/some-tasks-require-permissions.md" >}}
|
||||
Some tasks require certain permissions. For more information about roles, refer to [Roles and permissions]({{< relref "../roles-and-permissions/" >}}).
|
||||
|
||||
### Change organization name
|
||||
|
||||
@@ -39,24 +39,20 @@ Grafana server administrators and organization administrators can change organiz
|
||||
|
||||
Follow these instructions if you are a Grafana Server Admin.
|
||||
|
||||
{{< docs/list >}}
|
||||
{{< docs/shared "manage-users/view-server-org-list.md" >}}
|
||||
|
||||
1. Hover your cursor over the **Server Admin** (shield) icon until a menu appears.
|
||||
1. Click **Orgs**.
|
||||
1. In the organization list, click the name of the organization that you want to change.
|
||||
1. In **Name**, enter the new organization name.
|
||||
1. Click **Update**.
|
||||
{{< /docs/list >}}
|
||||
|
||||
#### Organization Admin change organization name
|
||||
|
||||
If you are an Organization Admin, follow these steps:
|
||||
|
||||
{{< docs/list >}}
|
||||
{{< docs/shared "preferences/org-preferences-list.md" >}}
|
||||
|
||||
1. Hover your cursor over the **Configuration** (gear) icon.
|
||||
1. Click **Preferences**.
|
||||
1. In **Organization name**, enter the new name.
|
||||
1. Click **Update organization name**.
|
||||
{{< /docs/list >}}
|
||||
|
||||
### Change team name or email
|
||||
|
||||
@@ -80,7 +76,7 @@ To learn how to edit your user information, refer to [Edit your profile]({{< rel
|
||||
|
||||
In Grafana, you can modify the UI theme configured in the Settings or Preferences. Set the UI theme for the server, an organization, a team, or your personal user account using the instructions in this topic.
|
||||
|
||||
{{< docs/shared "preferences/some-tasks-require-permissions.md" >}}
|
||||
Some tasks require certain permissions. For more information about roles, refer to [Roles and permissions]({{< relref "../roles-and-permissions/" >}}).
|
||||
|
||||
### Theme options
|
||||
|
||||
@@ -112,36 +108,34 @@ To see what the current settings are, refer to [View server settings]({{< relref
|
||||
|
||||
Organization administrators can change the UI theme for all users in an organization.
|
||||
|
||||
{{< docs/list >}}
|
||||
{{< docs/shared "preferences/org-preferences-list.md" >}}
|
||||
{{< docs/shared "preferences/select-ui-theme-list.md" >}}
|
||||
{{< /docs/list >}}
|
||||
1. Hover your cursor over the **Configuration** (gear) icon.
|
||||
1. Click **Preferences**.
|
||||
1. In the Preferences section, select the **UI theme**.
|
||||
1. Click **Save**.
|
||||
|
||||
### Change team UI theme
|
||||
|
||||
Organization and team administrators can change the UI theme for all users in a team.
|
||||
|
||||
{{< docs/list >}}
|
||||
{{< docs/shared "manage-users/view-team-list.md" >}}
|
||||
|
||||
1. Hover your cursor over the **Configuration** (gear) icon in the side menu.
|
||||
1. Click **Teams**. Grafana displays the team list.
|
||||
1. Click on the team that you want to change the UI theme for and then navigate to the **Settings** tab.
|
||||
{{< docs/shared "preferences/select-ui-theme-list.md" >}}
|
||||
{{< /docs/list >}}
|
||||
1. In the Preferences section, select the **UI theme**.
|
||||
1. Click **Save**.
|
||||
|
||||
### Change your personal UI theme
|
||||
|
||||
You can change the UI theme for your user account. This setting overrides UI theme settings at higher levels.
|
||||
|
||||
{{< docs/list >}}
|
||||
{{< docs/shared "preferences/navigate-user-preferences-list.md" >}}
|
||||
{{< docs/shared "preferences/select-ui-theme-list.md" >}}
|
||||
{{< /docs/list >}}
|
||||
1. On the left menu, hover your cursor over your avatar and then click **Preferences**.
|
||||
1. In the Preferences section, select the **UI theme**.
|
||||
1. Click **Save**.
|
||||
|
||||
## Change the Grafana default timezone
|
||||
|
||||
By default, Grafana uses the timezone in your web browser. However, you can override this setting at the server, organization, team, or individual user level. This topic provides instructions for each task.
|
||||
|
||||
{{< docs/shared "preferences/some-tasks-require-permissions.md" >}}
|
||||
Some tasks require certain permissions. For more information about roles, refer to [Roles and permissions]({{< relref "../roles-and-permissions/" >}}).
|
||||
|
||||
### Set server timezone
|
||||
|
||||
@@ -151,36 +145,34 @@ Grafana server administrators can choose a default timezone for all users on the
|
||||
|
||||
Organization administrators can choose a default timezone for their organization.
|
||||
|
||||
{{< docs/list >}}
|
||||
{{< docs/shared "preferences/org-preferences-list.md" >}}
|
||||
{{< docs/shared "preferences/select-timezone-list.md" >}}
|
||||
{{< /docs/list >}}
|
||||
1. Hover your cursor over the **Configuration** (gear) icon.
|
||||
1. Click **Preferences**.
|
||||
1. Click to select an option in the **Timezone** list. **Default** is either the browser local timezone or the timezone selected at a higher level. Refer to [Time range controls]({{< relref "../../dashboards/time-range-controls/" >}}) for more information about Grafana time settings.
|
||||
1. Click **Save**.
|
||||
|
||||
### Set team timezone
|
||||
|
||||
Organization administrators and team administrators can choose a default timezone for all users in a team.
|
||||
|
||||
{{< docs/list >}}
|
||||
{{< docs/shared "manage-users/view-team-list.md" >}}
|
||||
|
||||
1. Hover your cursor over the **Configuration** (gear) icon in the side menu.
|
||||
1. Click **Teams**. Grafana displays the team list.
|
||||
1. Click on the team you that you want to change the timezone for and then navigate to the **Settings** tab.
|
||||
{{< docs/shared "preferences/select-timezone-list.md" >}}
|
||||
{{< /docs/list >}}
|
||||
1. Click to select an option in the **Timezone** list. **Default** is either the browser local timezone or the timezone selected at a higher level. Refer to [Time range controls]({{< relref "../../dashboards/time-range-controls/" >}}) for more information about Grafana time settings.
|
||||
1. Click **Save**.
|
||||
|
||||
### Set your personal timezone
|
||||
|
||||
You can change the timezone for your user account. This setting overrides timezone settings at higher levels.
|
||||
|
||||
{{< docs/list >}}
|
||||
{{< docs/shared "preferences/navigate-user-preferences-list.md" >}}
|
||||
{{< docs/shared "preferences/select-timezone-list.md" >}}
|
||||
{{< /docs/list >}}
|
||||
1. On the left menu, hover your cursor over your avatar and then click **Preferences**.
|
||||
1. Click to select an option in the **Timezone** list. **Default** is either the browser local timezone or the timezone selected at a higher level. Refer to [Time range controls]({{< relref "../../dashboards/time-range-controls/" >}}) for more information about Grafana time settings.
|
||||
1. Click **Save**.
|
||||
|
||||
## Change the default home dashboard
|
||||
|
||||
The home dashboard you set is the one all users will see by default when they log in. You can set the home dashboard for the server, an organization, a team, or your personal user account. This topic provides instructions for each task.
|
||||
|
||||
{{< docs/shared "preferences/some-tasks-require-permissions.md" >}}
|
||||
Some tasks require certain permissions. For more information about roles, refer to [Roles and permissions]({{< relref "../roles-and-permissions/" >}}).
|
||||
|
||||
### Navigate to the home dashboard
|
||||
|
||||
@@ -216,30 +208,31 @@ default_home_dashboard_path = data/main-dashboard.json
|
||||
|
||||
Organization administrators can choose a home dashboard for their organization.
|
||||
|
||||
{{< docs/list >}}
|
||||
{{< docs/shared "preferences/navigate-to-the-dashboard-list.md" >}}
|
||||
{{< docs/shared "preferences/org-preferences-list.md" >}}
|
||||
{{< docs/shared "preferences/select-home-dashboard-list.md" >}}
|
||||
{{< /docs/list >}}
|
||||
1. Navigate to the dashboard you want to set as the home dashboard.
|
||||
1. Click the star next to the dashboard title to mark the dashboard as a favorite if it is not already.
|
||||
1. Hover your cursor over the **Configuration** (gear) icon.
|
||||
1. Click **Preferences**.
|
||||
1. In the **Home Dashboard** field, select the dashboard that you want to use for your home dashboard. Options include all starred dashboards.
|
||||
1. Click **Save**.
|
||||
|
||||
### Set home dashboard for your team
|
||||
|
||||
Organization administrators and Team Admins can choose a home dashboard for a team.
|
||||
|
||||
{{< docs/list >}}
|
||||
{{< docs/shared "preferences/navigate-to-the-dashboard-list.md" >}}
|
||||
{{< docs/shared "manage-users/view-team-list.md" >}}
|
||||
|
||||
1. Navigate to the dashboard you want to set as the home dashboard.
|
||||
1. Click the star next to the dashboard title to mark the dashboard as a favorite if it is not already.
|
||||
1. Hover your cursor over the **Configuration** (gear) icon in the side menu.
|
||||
1. Click **Teams**. Grafana displays the team list.
|
||||
1. Click on the team that you want to change the home dashboard for and then navigate to the **Settings** tab.
|
||||
{{< docs/shared "preferences/select-home-dashboard-list.md" >}}
|
||||
{{< /docs/list >}}
|
||||
1. In the **Home Dashboard** field, select the dashboard that you want to use for your home dashboard. Options include all starred dashboards.
|
||||
1. Click **Save**.
|
||||
|
||||
### Set your personal home dashboard
|
||||
|
||||
You can choose your own personal home dashboard. This setting overrides all home dashboards set at higher levels.
|
||||
|
||||
{{< docs/list >}}
|
||||
{{< docs/shared "preferences/navigate-to-the-dashboard-list.md" >}}
|
||||
{{< docs/shared "preferences/navigate-user-preferences-list.md" >}}
|
||||
{{< docs/shared "preferences/select-home-dashboard-list.md" >}}
|
||||
{{< /docs/list >}}
|
||||
1. Navigate to the dashboard you want to set as the home dashboard.
|
||||
1. Click the star next to the dashboard title to mark the dashboard as a favorite if it is not already.
|
||||
1. On the left menu, hover your cursor over your avatar and then click **Preferences**.
|
||||
1. In the **Home Dashboard** field, select the dashboard that you want to use for your home dashboard. Options include all starred dashboards.
|
||||
1. Click **Save**.
|
||||
|
||||
@@ -15,7 +15,7 @@ weight: 400
|
||||
|
||||
# Create a Grafana managed alerting rule
|
||||
|
||||
Grafana allows you to create alerting rules that query one or more data sources, reduce or transform the results and compare them to each other or to fix thresholds. When these are executed, Grafana sends notifications to the contact point. For information on Grafana Alerting, see [About Grafana Alerting]({{< relref "../about-alerting/" >}}) which explains the various components of Grafana Alerting. We also recommend that you familiarize yourself with some of the [fundamental concepts]({{< relref "../fundamentals/" >}}) of Grafana Alerting.
|
||||
Grafana allows you to create alerting rules that query one or more data sources, reduce or transform the results and compare them to each other or to fix thresholds. When these are executed, Grafana sends notifications to the contact point. For information on Grafana Alerting, see [About Grafana Alerting]({{< relref "../" >}}) which explains the various components of Grafana Alerting. We also recommend that you familiarize yourself with some of the [fundamental concepts]({{< relref "../fundamentals/" >}}) of Grafana Alerting.
|
||||
|
||||
Watch this video to learn more about creating alerts: {{< vimeo 720001934 >}}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ weight: 400
|
||||
|
||||
# Create a Grafana Mimir or Loki managed alerting rule
|
||||
|
||||
Grafana allows you to create alerting rules for an external Grafana Mimir or Loki instance that has ruler API enabled. For information on Grafana Alerting, see [About Grafana Alerting]({{< relref "../about-alerting/" >}}) which explains the various components of Grafana Alerting. We also recommend that you familiarize yourself with some of the [fundamental concepts]({{< relref "../fundamentals/" >}}) of Grafana Alerting.
|
||||
Grafana allows you to create alerting rules for an external Grafana Mimir or Loki instance that has ruler API enabled. For information on Grafana Alerting, see [About Grafana Alerting]({{< relref "../" >}}) which explains the various components of Grafana Alerting. We also recommend that you familiarize yourself with some of the [fundamental concepts]({{< relref "../fundamentals/" >}}) of Grafana Alerting.
|
||||
|
||||
## Before you begin
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
aliases:
|
||||
- /docs/grafana/latest/alerting/fundamentals/data-source-alerting/
|
||||
description: Data sources in Grafana Alerting
|
||||
title: Data sources
|
||||
weight: 100
|
||||
---
|
||||
|
||||
# Data sources
|
||||
|
||||
There are a number of data sources that are compatible with Grafana Alerting. Each data source is supported by a plugin. You can use one of the built-in data sources listed below, use [external data source plugins](https://grafana.com/grafana/plugins/?type=datasource), or create your own data source plugin.
|
||||
|
||||
If you are creating your own data source plugin, make sure it is a backend plugin as Grafana Alerting requires this in order to be able to evaluate rules using the data source. Frontend data sources are not supported, because the evaluation engine runs on the backend.
|
||||
|
||||
Specifying { "alerting": true, “backend”: true } in the plugin.json file indicates that the data source plugin is compatible with Grafana Alerting and includes the backend data-fetching code. For more information, refer to [Build a data source backend plugin](https://grafana.com/tutorials/build-a-data-source-backend-plugin/).
|
||||
|
||||
These are the data sources that are compatible with and supported by Grafana Alerting.
|
||||
|
||||
- [AWS CloudWatch]({{< relref "../../datasources/aws-cloudwatch/" >}})
|
||||
- [Azure Monitor]({{< relref "../../datasources/azuremonitor/" >}})
|
||||
- [Elasticsearch]({{< relref "../../datasources/elasticsearch/" >}})
|
||||
- [Google Cloud Monitoring]({{< relref "../../google-cloud-monitoring/" >}})
|
||||
- [Graphite]({{< relref "../../datasources/graphite/" >}})
|
||||
- [InfluxDB]({{< relref "influxdb/" >}})
|
||||
- [Loki]({{< relref "../../datasources/loki/" >}})
|
||||
- ]Microsoft SQL Server (MSSQL)]({{< relref "../../datasources/mssql/" >}})
|
||||
- [MySQL]({{< relref "../../datasources/mysql/" >}})
|
||||
- [Open TSDB]({{< relref "../../datasources/opentsdb/" >}})
|
||||
- [PostgreSQL]({{< relref "../../datasources/postgres/" >}})
|
||||
- [Prometheus]({{< relref "../../datasources/prometheus/" >}})
|
||||
- [Jaeger]({{< relref "../../datasources/jaeger/" >}})
|
||||
- [Zipkin]({{< relref "../../datasources/zipkin/" >}})
|
||||
- [Tempo]({{< relref "../../datasources/tempo/" >}})
|
||||
- [Testdata]({{< relref "../../datasources/testdata/" >}})
|
||||
|
||||
## Useful links
|
||||
|
||||
- [Grafana data sources]({{< relref "../../data-sources/" >}})
|
||||
- [Add a data source]({{< relref "../../data-sources/add-a-data-source/" >}})
|
||||
@@ -11,7 +11,7 @@ title: Images in notifications
|
||||
|
||||
# Images in notifications
|
||||
|
||||
Images in notifications helps recipients of alert notifications better understand why an alert has fired or resolved by including an image of the panel for the Grafana managed alert rule.
|
||||
Images in notifications helps recipients of alert notifications better understand why an alert has fired or resolved by including an image of the panel associated with the Grafana managed alert rule.
|
||||
|
||||
> **Note**: Images in notifications are not available for Grafana Mimir and Loki managed alert rules, or when Grafana is set up to send alert notifications to an external Alertmanager.
|
||||
|
||||
@@ -20,6 +20,8 @@ If Grafana is set up to send images in notifications, it takes a screenshot of t
|
||||
1. The alert rule transitions from pending to firing
|
||||
2. The alert rule transitions from firing to OK
|
||||
|
||||
Grafana does not support images for alert rules that are not associated with a panel. An alert rule is associated with a panel when it has both Dashboard UID and Panel ID annotations.
|
||||
|
||||
Images are stored in the [data]({{< relref "../setup-grafana/configure-grafana/#paths" >}}) path and so Grafana must have write-access to this path. If Grafana cannot write to this path then screenshots cannot be saved to disk and an error will be logged for each failed screenshot attempt. In addition to storing images on disk, Grafana can also store the image in an external image store such as Amazon S3, Azure Blob Storage, Google Cloud Storage and even Grafana where screenshots are stored in `public/img/attachments`. Screenshots older than `temp_data_lifetime` are deleted from disk but not the external image store. If Grafana is the external image store then screenshots are deleted from `data` but not from `public/img/attachments`.
|
||||
|
||||
> **Note**: It is recommended that you use an external image store, as not all contact points support uploading images from disk. It is also possible that the image on disk is deleted before an alert notification is sent if `temp_data_lifetime` is less than the `group_wait` and `group_interval` options used in Alertmanager.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
aliases:
|
||||
- /docs/grafana/latest/alerting/migrating-alerts/differences-and-limitations/
|
||||
- /docs/grafana/latest/alerting/migrating-alerts/migrating-legacy-alerts/
|
||||
- /docs/grafana/latest/alerting/migrating-legacy-alerts/
|
||||
- /docs/grafana/latest/alerting/unified-alerting/opt-in/
|
||||
@@ -10,22 +11,27 @@ weight: 106
|
||||
|
||||
# Differences and limitations
|
||||
|
||||
When Grafana Alerting is enabled or upgraded to Grafana 9.0 or later, existing legacy dashboard alerts migrate in a format compatible with the Grafana Alerting. In the Alerting page of your Grafana instance, you can view the migrated alerts alongside any new alerts.
|
||||
This topic explains how legacy dashboard alerts are migrated and some limitations of the migration.
|
||||
There are some differences between Grafana Alerting and legacy dashboard alerts, and a number of features that are no
|
||||
longer supported. We refer to these as [Differences]({{< relref "#differences" >}}) and [Limitations]({{< relref "#limitations" >}}).
|
||||
|
||||
> **Note:** This topic is only relevant for OSS and Enterprise customers. Contact customer support to enable or disable Grafana Alerting for your Cloud stack.
|
||||
## Differences
|
||||
|
||||
Read and write access to legacy dashboard alerts and Grafana alerts are governed by the permissions of the folders storing them. During migration, legacy dashboard alert permissions are matched to the new rules permissions as follows:
|
||||
1. When Grafana Alerting is enabled or upgraded to Grafana 9.0 or later, existing legacy dashboard alerts migrate in a format compatible with the Grafana Alerting. In the Alerting page of your Grafana instance, you can view the migrated alerts alongside any new alerts.
|
||||
This topic explains how legacy dashboard alerts are migrated and some limitations of the migration.
|
||||
|
||||
2. Read and write access to legacy dashboard alerts and Grafana alerts are governed by the permissions of the folders storing them. During migration, legacy dashboard alert permissions are matched to the new rules permissions as follows:
|
||||
|
||||
- If alert's dashboard has permissions, it will create a folder named like `Migrated {"dashboardUid": "UID", "panelId": 1, "alertId": 1}` to match permissions of the dashboard (including the inherited permissions from the folder).
|
||||
- If there are no dashboard permissions and the dashboard is under a folder, then the rule is linked to this folder and inherits its permissions.
|
||||
- If there are no dashboard permissions and the dashboard is under the General folder, then the rule is linked to the `General Alerting` folder, and the rule inherits the default permissions.
|
||||
|
||||
> **Note:** Since there is no `Keep Last State` option for [`No Data`]({{< relref "../alerting-rules/create-grafana-managed-rule/#no-data--error-handling" >}}) in Grafana Alerting, this option becomes `NoData` during the legacy rules migration. Option "Keep Last State" for [`Error handling`]({{< relref "../alerting-rules/create-grafana-managed-rule/#no-data--error-handling" >}}) is migrated to a new option `Error`. To match the behavior of the `Keep Last State`, in both cases, during the migration Grafana automatically creates a [silence]({{< relref "../silences/" >}}) for each alert rule with a duration of 1 year.
|
||||
3. Since there is no `Keep Last State` option for [`No Data`]({{< relref "../alerting-rules/create-grafana-managed-rule/#no-data--error-handling" >}}) in Grafana Alerting, this option becomes `NoData` during the legacy rules migration. Option "Keep Last State" for [`Error handling`]({{< relref "../alerting-rules/create-grafana-managed-rule/#no-data--error-handling" >}}) is migrated to a new option `Error`. To match the behavior of the `Keep Last State`, in both cases, during the migration Grafana automatically creates a [silence]({{< relref "../silences/" >}}) for each alert rule with a duration of 1 year.
|
||||
|
||||
Notification channels are migrated to an Alertmanager configuration with the appropriate routes and receivers. Default notification channels are added as contact points to the default route. Notification channels not associated with any Dashboard alert go to the `autogen-unlinked-channel-recv` route.
|
||||
4. Notification channels are migrated to an Alertmanager configuration with the appropriate routes and receivers. Default notification channels are added as contact points to the default route. Notification channels not associated with any Dashboard alert go to the `autogen-unlinked-channel-recv` route.
|
||||
|
||||
5. Unlike legacy dashboard alerts where images in notifications are enabled per contact point, images in notifications for Grafana Alerting must be enabled in the Grafana configuration, either in the configuration file or environment variables, and are enabled for either all or no contact points. Please refer to the [documentation for images in notifications]({{< relref "../images-in-notifications" >}}).
|
||||
|
||||
## Limitations
|
||||
|
||||
Since `Hipchat` and `Sensu` notification channels are no longer supported, legacy alerts associated with these channels are not automatically migrated to Grafana Alerting. Assign the legacy alerts to a supported notification channel so that you continue to receive notifications for those alerts.
|
||||
Silences (expiring after one year) are created for all paused dashboard alerts.
|
||||
1. Since `Hipchat` and `Sensu` notification channels are no longer supported, legacy alerts associated with these channels are not automatically migrated to Grafana Alerting. Assign the legacy alerts to a supported notification channel so that you continue to receive notifications for those alerts.
|
||||
Silences (expiring after one year) are created for all paused dashboard alerts.
|
||||
|
||||
@@ -8,7 +8,7 @@ weight: 60
|
||||
|
||||
# Data sources
|
||||
|
||||
Grafana supports many different storage backends for your time series data (data source). Refer to [Add a data source]({{< relref "../../administration/datasources/add-a-data-source/" >}}) for instructions on how to add a data source to Grafana. Only users with the organization admin role can add data sources.
|
||||
Grafana supports many different storage backends for your time series data (data source). Refer to [Add a data source]({{< relref "../administration/data-source-management/#add-a-data-source/" >}}) for instructions on how to add a data source to Grafana. Only users with the organization admin role can add data sources.
|
||||
|
||||
## Querying
|
||||
|
||||
@@ -18,23 +18,23 @@ Each data source has a specific Query Editor that is customized for the features
|
||||
|
||||
The following data sources are officially supported:
|
||||
|
||||
- [Alertmanager]({{< relref "../../datasources/alertmanager/" >}})
|
||||
- [AWS CloudWatch]({{< relref "aws-cloudwatch/" >}})
|
||||
- [Azure Monitor]({{< relref "azuremonitor/" >}})
|
||||
- [Elasticsearch]({{< relref "../../datasources/elasticsearch/" >}})
|
||||
- [Google Cloud Monitoring]({{< relref "google-cloud-monitoring/" >}})
|
||||
- [Graphite]({{< relref "../../datasources/graphite/" >}})
|
||||
- [InfluxDB]({{< relref "influxdb/" >}})
|
||||
- [Loki]({{< relref "../../datasources/loki/" >}})
|
||||
- [Microsoft SQL Server (MSSQL)]({{< relref "../../datasources/mssql/" >}})
|
||||
- [MySQL]({{< relref "../../datasources/mysql/" >}})
|
||||
- [OpenTSDB]({{< relref "../../datasources/opentsdb/" >}})
|
||||
- [PostgreSQL]({{< relref "../../datasources/postgres/" >}})
|
||||
- [Prometheus]({{< relref "../../datasources/prometheus/" >}})
|
||||
- [Jaeger]({{< relref "../../datasources/jaeger/" >}})
|
||||
- [Zipkin]({{< relref "../../datasources/zipkin/" >}})
|
||||
- [Tempo]({{< relref "../../datasources/tempo/" >}})
|
||||
- [Testdata]({{< relref "../../datasources/testdata/" >}})
|
||||
- [Alertmanager]({{< relref "./alertmanager/" >}})
|
||||
- [AWS CloudWatch]({{< relref "./aws-cloudwatch/" >}})
|
||||
- [Azure Monitor]({{< relref "./azuremonitor/" >}})
|
||||
- [Elasticsearch]({{< relref "./elasticsearch/" >}})
|
||||
- [Google Cloud Monitoring]({{< relref "./google-cloud-monitoring/" >}})
|
||||
- [Graphite]({{< relref "./graphite/" >}})
|
||||
- [InfluxDB]({{< relref "./influxdb/" >}})
|
||||
- [Loki]({{< relref "./loki/" >}})
|
||||
- [Microsoft SQL Server (MSSQL)]({{< relref "./mssql/" >}})
|
||||
- [MySQL]({{< relref "./mysql/" >}})
|
||||
- [OpenTSDB]({{< relref "./opentsdb/" >}})
|
||||
- [PostgreSQL]({{< relref "./postgres/" >}})
|
||||
- [Prometheus]({{< relref "./prometheus/" >}})
|
||||
- [Jaeger]({{< relref "./jaeger/" >}})
|
||||
- [Zipkin]({{< relref "./zipkin/" >}})
|
||||
- [Tempo]({{< relref "./tempo/" >}})
|
||||
- [Testdata]({{< relref "./testdata/" >}})
|
||||
|
||||
In addition to the data sources that you have configured in your Grafana, there are three special data sources available:
|
||||
|
||||
|
||||
@@ -123,6 +123,10 @@ Operation can have additional parameters under the operation header. See the ope
|
||||
|
||||
Some operations make sense only in specific order, if adding an operation would result in nonsensical query, operation will be added to the correct place. To order operations manually drag operation box by the operation name and drop in appropriate place.
|
||||
|
||||
##### Hints
|
||||
|
||||
In same cases the query editor can detect which operations would be most appropriate for a selected log stream. In such cases it will show a hint next to the `+ Operations` button. Click on the hint to add the operations to your query.
|
||||
|
||||
#### Raw query
|
||||
|
||||
This section is shown only if the `Raw query` switch from the query editor top toolbar is set to `on`. It shows the raw query that will be created and executed by the query editor.
|
||||
|
||||
@@ -471,6 +471,8 @@ Content-Type: application/json
|
||||
|
||||
`POST /api/admin/pause-all-alerts`
|
||||
|
||||
> **Note:** This API is relevant for the [legacy dashboard alerts]({{< relref "../../old-alerting/" >}}) only. For default alerting, use [silences]({{< relref "../../alerting/silences/" >}}) to stop alerts from being delivered.
|
||||
|
||||
Only works with Basic Authentication (username and password). See [introduction](http://docs.grafana.org/http_api/admin/#admin-api) for an explanation.
|
||||
|
||||
**Example Request**:
|
||||
|
||||
@@ -15,7 +15,7 @@ title: 'Alerting HTTP API '
|
||||
|
||||
# Alerting API
|
||||
|
||||
> **Note:** This topic is relevant for the [legacy dashboard alerts]({{< ref "/docs/grafana/v8.5/alerting/old-alerting/" >}}) only.
|
||||
> **Note:** This topic is relevant for the [legacy dashboard alerts]({{< relref "../../old-alerting/" >}}) only.
|
||||
|
||||
You can find Grafana Alerting API specification details [here](https://editor.swagger.io/?url=https://raw.githubusercontent.com/grafana/grafana/main/pkg/services/ngalert/api/tooling/post.json). Also, refer to [Grafana Alerting alerts documentation]({{< relref "../../alerting/" >}}) for details on how to create and manage new alerts.
|
||||
|
||||
|
||||
@@ -11,14 +11,14 @@ keywords:
|
||||
- annotation
|
||||
- annotations
|
||||
- comment
|
||||
title: 'Annotations HTTP API '
|
||||
title: 'Annotations HTTP API'
|
||||
---
|
||||
|
||||
# Annotations API
|
||||
|
||||
This is the API documentation for the new Grafana Annotations feature released in Grafana 4.6. Annotations are saved in the Grafana database (sqlite, mysql or postgres). Annotations can be organization annotations that can be shown on any dashboard by configuring an annotation data source - they are filtered by tags. Or they can be tied to a panel on a dashboard and are then only shown on that panel.
|
||||
|
||||
> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes/" >}}) for more information.
|
||||
> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../administration/roles-and-permissions/access-control/custom-role-actions-scopes/" >}}) for more information.
|
||||
|
||||
## Find Annotations
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ title: 'Authentication HTTP API '
|
||||
|
||||
# Authentication API
|
||||
|
||||
> If you are running Grafana Enterprise, for some endpoints you would need to have relevant permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes/" >}}) for more information.
|
||||
> If you are running Grafana Enterprise, for some endpoints you would need to have relevant permissions. Refer to [Role-based access control permissions]({{< relref "../../administration/roles-and-permissions/access-control/custom-role-actions-scopes/" >}}) for more information.
|
||||
|
||||
## Tokens
|
||||
|
||||
|
||||
@@ -9,12 +9,12 @@ keywords:
|
||||
- documentation
|
||||
- api
|
||||
- dashboard
|
||||
title: 'Dashboard HTTP API '
|
||||
title: 'Dashboard HTTP API'
|
||||
---
|
||||
|
||||
# Dashboard API
|
||||
|
||||
> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes/" >}}) for more information.
|
||||
> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../administration/roles-and-permissions/access-control/custom-role-actions-scopes/" >}}) for more information.
|
||||
|
||||
## Identifier (id) vs unique identifier (uid)
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ keywords:
|
||||
- permission
|
||||
- permissions
|
||||
- acl
|
||||
title: 'Dashboard Permissions HTTP API '
|
||||
title: 'Dashboard Permissions HTTP API'
|
||||
---
|
||||
|
||||
# Dashboard Permissions API
|
||||
@@ -28,7 +28,7 @@ The permission levels for the permission field:
|
||||
- 2 = Edit
|
||||
- 4 = Admin
|
||||
|
||||
> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes/" >}}) for more information.
|
||||
> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../administration/roles-and-permissions/access-control/custom-role-actions-scopes/" >}}) for more information.
|
||||
|
||||
## Get permissions for a dashboard
|
||||
|
||||
|
||||
@@ -10,12 +10,12 @@ keywords:
|
||||
- documentation
|
||||
- api
|
||||
- data source
|
||||
title: 'Data source HTTP API '
|
||||
title: 'Data source HTTP API'
|
||||
---
|
||||
|
||||
# Data source API
|
||||
|
||||
> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes/" >}}) for more information.
|
||||
> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../administration/roles-and-permissions/access-control/custom-role-actions-scopes/" >}}) for more information.
|
||||
|
||||
## Get all data sources
|
||||
|
||||
|
||||
@@ -14,14 +14,14 @@ keywords:
|
||||
- permissions
|
||||
- acl
|
||||
- enterprise
|
||||
title: 'Datasource Permissions HTTP API '
|
||||
title: 'Datasource Permissions HTTP API'
|
||||
---
|
||||
|
||||
# Data Source Permissions API
|
||||
|
||||
> The Data Source Permissions is only available in Grafana Enterprise. Read more about [Grafana Enterprise]({{< relref "../../enterprise/" >}}).
|
||||
|
||||
> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes/" >}}) for more information.
|
||||
> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../administration/roles-and-permissions/access-control/custom-role-actions-scopes/" >}}) for more information.
|
||||
|
||||
This API can be used to enable, disable, list, add and remove permissions for a data source.
|
||||
|
||||
|
||||
@@ -13,14 +13,14 @@ keywords:
|
||||
- group
|
||||
- member
|
||||
- enterprise
|
||||
title: 'External Group Sync HTTP API '
|
||||
title: 'External Group Sync HTTP API'
|
||||
---
|
||||
|
||||
# External Group Synchronization API
|
||||
|
||||
> External Group Synchronization is only available in Grafana Enterprise. Read more about [Grafana Enterprise]({{< relref "../../enterprise/" >}}).
|
||||
|
||||
> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes/" >}}) for more information.
|
||||
> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../administration/roles-and-permissions/access-control/custom-role-actions-scopes/" >}}) for more information.
|
||||
|
||||
## Get External Groups
|
||||
|
||||
|
||||
@@ -9,12 +9,12 @@ keywords:
|
||||
- documentation
|
||||
- api
|
||||
- folder
|
||||
title: 'Folder HTTP API '
|
||||
title: 'Folder HTTP API'
|
||||
---
|
||||
|
||||
# Folder API
|
||||
|
||||
> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes/" >}}) for more information.
|
||||
> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../administration/roles-and-permissions/access-control/custom-role-actions-scopes/" >}}) for more information.
|
||||
|
||||
## Identifier (id) vs unique identifier (uid)
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ keywords:
|
||||
- search
|
||||
- folder
|
||||
- dashboard
|
||||
title: 'Folder/Dashboard Search HTTP API '
|
||||
title: 'Folder/Dashboard Search HTTP API'
|
||||
---
|
||||
|
||||
# Folder/Dashboard Search API
|
||||
@@ -20,7 +20,7 @@ title: 'Folder/Dashboard Search HTTP API '
|
||||
|
||||
`GET /api/search/`
|
||||
|
||||
> Note: When using [Role-based access control]({{< relref "../../enterprise/access-control/" >}}), search results will contain only dashboards and folders which you have access to.
|
||||
> Note: When using [Role-based access control]({{< relref "../../administration/roles-and-permissions/access-control/" >}}), search results will contain only dashboards and folders which you have access to.
|
||||
|
||||
Query parameters:
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ keywords:
|
||||
- permission
|
||||
- permissions
|
||||
- acl
|
||||
title: 'Folder Permissions HTTP API '
|
||||
title: 'Folder Permissions HTTP API'
|
||||
---
|
||||
|
||||
# Folder Permissions API
|
||||
@@ -28,7 +28,7 @@ The permission levels for the permission field:
|
||||
- 2 = Edit
|
||||
- 4 = Admin
|
||||
|
||||
> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes/" >}}) for more information.
|
||||
> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../administration/roles-and-permissions/access-control/custom-role-actions-scopes/" >}}) for more information.
|
||||
|
||||
## Get permissions for a folder
|
||||
|
||||
|
||||
@@ -10,14 +10,14 @@ keywords:
|
||||
- api
|
||||
- licensing
|
||||
- enterprise
|
||||
title: 'Licensing HTTP API '
|
||||
title: 'Licensing HTTP API'
|
||||
---
|
||||
|
||||
# Enterprise License API
|
||||
|
||||
Licensing is only available in Grafana Enterprise. Read more about [Grafana Enterprise]({{< relref "../../enterprise/" >}}).
|
||||
|
||||
> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes/" >}}) for more information.
|
||||
> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../administration/roles-and-permissions/access-control/custom-role-actions-scopes/" >}}) for more information.
|
||||
|
||||
## Check license availability
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ keywords:
|
||||
- documentation
|
||||
- api
|
||||
- organization
|
||||
title: 'Organization HTTP API '
|
||||
title: 'Organization HTTP API'
|
||||
---
|
||||
|
||||
# Organization API
|
||||
@@ -19,7 +19,7 @@ The Organization HTTP API is divided in two resources, `/api/org` (current organ
|
||||
and `/api/orgs` (admin organizations). One big difference between these are that
|
||||
the admin of all organizations API only works with basic authentication, see [Admin Organizations API](#admin-organizations-api) for more information.
|
||||
|
||||
> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes/" >}}) for more information.
|
||||
> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../administration/roles-and-permissions/access-control/custom-role-actions-scopes/" >}}) for more information.
|
||||
|
||||
## Current Organization API
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ This API allows you to interact programmatically with the [Reporting]({{< relref
|
||||
|
||||
> Reporting is only available in Grafana Enterprise. Read more about [Grafana Enterprise]({{< relref "../../enterprise/" >}}).
|
||||
|
||||
> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes/" >}}) for more information.
|
||||
> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../administration/roles-and-permissions/access-control/custom-role-actions-scopes/" >}}) for more information.
|
||||
|
||||
## Send a report
|
||||
|
||||
|
||||
@@ -9,12 +9,12 @@ keywords:
|
||||
- documentation
|
||||
- api
|
||||
- serviceaccount
|
||||
title: 'Service account HTTP API '
|
||||
title: 'Service account HTTP API'
|
||||
---
|
||||
|
||||
# Service account API
|
||||
|
||||
> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes/" >}}) for more information.
|
||||
> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../administration/roles-and-permissions/access-control/custom-role-actions-scopes/" >}}) for more information.
|
||||
|
||||
## Search service accounts with Paging
|
||||
|
||||
@@ -105,7 +105,8 @@ Authorization: Basic YWRtaW46YWRtaW4=
|
||||
|
||||
{
|
||||
"name": "grafana",
|
||||
"role": "Admin",
|
||||
"role": "Viewer",
|
||||
"isDisabled" : false
|
||||
}
|
||||
```
|
||||
|
||||
@@ -131,7 +132,7 @@ Content-Type: application/json
|
||||
}
|
||||
```
|
||||
|
||||
## Get single serviceaccount by Id
|
||||
## Get a service account by ID
|
||||
|
||||
`GET /api/serviceaccounts/:id`
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ keywords:
|
||||
- team
|
||||
- teams
|
||||
- group
|
||||
title: 'Team HTTP API '
|
||||
title: 'Team HTTP API'
|
||||
---
|
||||
|
||||
# Team API
|
||||
@@ -25,7 +25,7 @@ Access to these API endpoints is restricted as follows:
|
||||
- If you enable `editors_can_admin` configuration flag, then Organization Editors can create teams and manage teams where they are Admin.
|
||||
- If you enable `editors_can_admin` configuration flag, Editors can find out whether a team that they are not members of exists by trying to create a team with the same name.
|
||||
|
||||
> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes/" >}}) for more information.
|
||||
> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../administration/roles-and-permissions/access-control/custom-role-actions-scopes/" >}}) for more information.
|
||||
|
||||
## Team Search With Paging
|
||||
|
||||
|
||||
@@ -9,12 +9,12 @@ keywords:
|
||||
- documentation
|
||||
- api
|
||||
- user
|
||||
title: 'User HTTP API '
|
||||
title: 'User HTTP API'
|
||||
---
|
||||
|
||||
# User API
|
||||
|
||||
> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes/" >}}) for more information.
|
||||
> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../administration/roles-and-permissions/access-control/custom-role-actions-scopes/" >}}) for more information.
|
||||
|
||||
## Search Users
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ aliases:
|
||||
- /docs/grafana/latest/developers/plugins/
|
||||
- /docs/grafana/latest/plugins/developing/
|
||||
title: Build a plugin
|
||||
description: Resources for creating Grafana plugins
|
||||
weight: 200
|
||||
---
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ This guide explains how to add support for [annotations]({{< relref "../../dashb
|
||||
|
||||
This guide assumes that you're already familiar with how to [Build a data source plugin]({{< ref "/tutorials/build-a-data-source-plugin/" >}}).
|
||||
|
||||
> **Note:** Annotation support for React plugins was released in Grafana 7.2. To support earlier versions, refer to the [Add support for annotation for Grafana 7.1](https://grafana.com/docs/grafana/v7.1/developers/plugins/add-support-for-annotations/).
|
||||
> **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/).
|
||||
|
||||
## Add annotations support to your data source
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ To learn more about Grafana Enterprise, refer to [our product page](https://graf
|
||||
|
||||
## Enterprise features in Grafana Cloud
|
||||
|
||||
Many Grafana Enterprise features are also available in [Grafana Cloud]({{< ref "/grafana-cloud" >}}) Pro and Advanced accounts. For details, refer to [the Grafana Cloud features table](https://grafana.com/pricing/#featuresTable) and [Enterprise features available to Grafana Cloud Pro and Advanced accounts]({{< ref "/grafana-cloud/reference/enterprise-features" >}}).
|
||||
Many Grafana Enterprise features are also available in [Grafana Cloud]({{< ref "/docs/grafana-cloud" >}}) Pro and Advanced accounts. For details, refer to [the Grafana Cloud features table](https://grafana.com/pricing/#featuresTable) and [Enterprise features available to Grafana Cloud Pro and Advanced accounts]({{< ref "/docs/grafana-cloud/reference/enterprise-features" >}}).
|
||||
|
||||
## Authentication
|
||||
|
||||
|
||||
@@ -11,19 +11,19 @@ weight: 5
|
||||
|
||||
[Grafana open source software](https://grafana.com/oss/) enables you to query, visualize, alert on, and explore your metrics, logs, and traces wherever they are stored. Grafana OSS provides you with tools to turn your time-series database (TSDB) data into insightful graphs and visualizations.
|
||||
|
||||
After you have [installed Grafana]({{< relref "../setup-grafana/installation/" >}}) and set up your first dashboard using instructions in [Getting started with Grafana]({{< relref "../getting-started/build-first-dashboard.md" >}}), you will have many options to choose from depending on your requirements. For example, if you want to view weather data and statistics about your smart home, then you can create a [playlist]({{< relref "../dashboards/playlist.md" >}}). If you are the administrator for an enterprise and are managing Grafana for multiple teams, then you can set up [provisioning]({{< relref "../administration/server-administration/provisioning.md" >}}) and [authentication]({{< relref "../setup-grafana/configure-security/configure-authentication/" >}}).
|
||||
After you have [installed Grafana]({{< relref "../setup-grafana/installation/" >}}) and set up your first dashboard using instructions in [Getting started with Grafana]({{< relref "../getting-started/build-first-dashboard.md" >}}), you will have many options to choose from depending on your requirements. For example, if you want to view weather data and statistics about your smart home, then you can create a [playlist]({{< relref "../dashboards/playlist.md" >}}). If you are the administrator for an enterprise and are managing Grafana for multiple teams, then you can set up [provisioning]({{< relref "../administration/provisioning/" >}}) and [authentication]({{< relref "../setup-grafana/configure-security/configure-authentication/" >}}).
|
||||
|
||||
The following sections provide an overview of Grafana features and links to product documentation to help you learn more. For more guidance and ideas, check out our [Grafana Community forums](https://community.grafana.com/).
|
||||
|
||||
## Explore metrics, logs, and traces
|
||||
|
||||
Explore your data through ad-hoc queries and dynamic drilldown. Split view and compare different time ranges, queries and data sources side by side. Refer to [Explore]({{< relref "../explore/_index.md" >}}) for more information.
|
||||
Explore your data through ad-hoc queries and dynamic drilldown. Split view and compare different time ranges, queries and data sources side by side. Refer to [Explore]({{< relref "../explore/" >}}) for more information.
|
||||
|
||||
## Alerts
|
||||
|
||||
If you're using Grafana Alerting, then you can have alerts sent through a number of different [alert notifiers]({{< relref "../alerting/contact-points/_index.md#list-of-notifiers-supported-by-grafana" >}}), including PagerDuty, SMS, email, VictorOps, OpsGenie, or Slack.
|
||||
|
||||
Alert hooks allow you to create different notifiers with a bit of code if you prefer some other channels of communication. Visually define [alert rules]({{< relref "../alerting/alerting-rules/_index.md" >}}) for your most important metrics.
|
||||
Alert hooks allow you to create different notifiers with a bit of code if you prefer some other channels of communication. Visually define [alert rules]({{< relref "../alerting/alerting-rules/" >}}) for your most important metrics.
|
||||
|
||||
## Annotations
|
||||
|
||||
@@ -33,7 +33,7 @@ This feature, which shows up as a graph marker in Grafana, is useful for correla
|
||||
|
||||
## Dashboard variables
|
||||
|
||||
[Template variables]({{< relref "../variables/_index.md" >}}) allow you to create dashboards that can be reused for lots of different use cases. Values aren't hard-coded with these templates, so for instance, if you have a production server and a test server, you can use the same dashboard for both.
|
||||
[Template variables]({{< relref "../variables/" >}}) allow you to create dashboards that can be reused for lots of different use cases. Values aren't hard-coded with these templates, so for instance, if you have a production server and a test server, you can use the same dashboard for both.
|
||||
|
||||
Templating allows you to drill down into your data, say, from all data to North America data, down to Texas data, and beyond. You can also share these dashboards across teams within your organization—or if you create a great dashboard template for a popular data source, you can contribute it to the whole community to customize and use.
|
||||
|
||||
@@ -57,11 +57,11 @@ In Grafana Enterprise, you can also map users to teams: If your company has its
|
||||
|
||||
While it's easy to click, drag, and drop to create a single dashboard, power users in need of many dashboards will want to automate the setup with a script. You can script anything in Grafana.
|
||||
|
||||
For example, if you're spinning up a new Kubernetes cluster, you can also spin up a Grafana automatically with a script that would have the right server, IP address, and data sources preset and locked in so users cannot change them. It's also a way of getting control over a lot of dashboards. Refer to [Provisioning]({{< relref "../administration/server-administration/provisioning.md" >}}) for more information.
|
||||
For example, if you're spinning up a new Kubernetes cluster, you can also spin up a Grafana automatically with a script that would have the right server, IP address, and data sources preset and locked in so users cannot change them. It's also a way of getting control over a lot of dashboards. Refer to [Provisioning]({{< relref "../administration/provisioning/" >}}) for more information.
|
||||
|
||||
## Permissions
|
||||
|
||||
When organizations have one Grafana and multiple teams, they often want the ability to both keep things separate and share dashboards. You can create a team of users and then set permissions on [folders and dashboards]({{< relref "../administration/user-management/manage-dashboard-permissions/_index.md" >}}), and down to the [data source level]({{< relref "../administration/data-source-management/datasource-permissions.md" >}}) if you're using [Grafana Enterprise]({{< relref "../enterprise/_index.md" >}}).
|
||||
When organizations have one Grafana and multiple teams, they often want the ability to both keep things separate and share dashboards. You can create a team of users and then set permissions on [folders and dashboards]({{< relref "../administration/user-management/manage-dashboard-permissions/" >}}), and down to the [data source level]({{< relref "../administration/data-source-management#data-source-permissions" >}}) if you're using [Grafana Enterprise]({{< relref "../enterprise/" >}}).
|
||||
|
||||
## Other Grafana Labs OSS Projects
|
||||
|
||||
|
||||
@@ -49,8 +49,8 @@ With [enhanced LDAP integration]({{< relref "../setup-grafana/configure-security
|
||||
|
||||
Grafana Enterprise adds the following features:
|
||||
|
||||
- [Role-based access control]({{< relref "../enterprise/access-control/" >}}) to control access with role-based permissions.
|
||||
- [Data source permissions]({{< relref "../administration/data-source-management/datasource-permissions.md" >}}) to restrict query access to specific teams and users.
|
||||
- [Role-based access control]({{< relref "../administration/roles-and-permissions/access-control/" >}}) to control access with role-based permissions.
|
||||
- [Data source permissions]({{< relref "../administration/data-source-management#data-source-permissions" >}}) to restrict query access to specific teams and users.
|
||||
- [Data source query caching]({{< relref "../enterprise/query-caching.md" >}}) to temporarily store query results in Grafana to reduce data source load and rate limiting.
|
||||
- [Reporting]({{< relref "../enterprise/reporting.md" >}}) to generate a PDF report from any dashboard and set up a schedule to have it emailed to whoever you choose.
|
||||
- [Export dashboard as PDF]({{< relref "../enterprise/export-pdf.md" >}})
|
||||
|
||||
@@ -72,6 +72,9 @@ bind_dn = "cn=admin,dc=grafana,dc=org"
|
||||
# If the password contains # or ; you have to wrap it with triple quotes. Ex """#password;"""
|
||||
bind_password = "grafana"
|
||||
|
||||
# Timeout in seconds. Applies to each host specified in the 'host' entry (space separated).
|
||||
timeout = 10
|
||||
|
||||
# User search filter, for example "(cn=%s)" or "(sAMAccountName=%s)" or "(uid=%s)"
|
||||
# Allow login from email or username, example "(|(sAMAccountName=%s)(userPrincipalName=%s))"
|
||||
search_filter = "(cn=%s)"
|
||||
|
||||
@@ -8,4 +8,5 @@ title: View org list as server admin
|
||||
{{< docs/shared "manage-users/view-server-org-list.md" >}}
|
||||
|
||||
1. Click the name of the organization that you want to edit.
|
||||
{{< /docs/list >}}
|
||||
|
||||
{{< /docs/list >}}
|
||||
|
||||
@@ -8,4 +8,5 @@ title: View user list and search - list format
|
||||
{{< docs/shared "manage-users/view-server-user-list.md" >}}
|
||||
|
||||
1. Click the user account that you want to edit. If necessary, use the search field to find the account.
|
||||
{{< /docs/list >}}
|
||||
|
||||
{{< /docs/list >}}
|
||||
|
||||
@@ -193,13 +193,13 @@ These features are included in the Grafana Enterprise edition.
|
||||
|
||||
### Licensing changes
|
||||
|
||||
When determining a user’s role for billing purposes, a user who has the ability to edit and save dashboards is considered an Editor. This includes any user who is an Editor or Admin at the Org level, and who has granted Admin or Edit permissions via [Dashboard permissions]({{< relref "../administration/manage-users-and-permissions/about-users-and-permissions/#dashboard-permissions" >}}).
|
||||
When determining a user’s role for billing purposes, a user who has the ability to edit and save dashboards is considered an Editor. This includes any user who is an Editor or Admin at the Org level, and who has granted Admin or Edit permissions via [Dashboard permissions]({{< relref "../administration/user-management/manage-dashboard-permissions/" >}}).
|
||||
|
||||
After the number of Viewers or Editors has reached its license limit, only Admins will see a banner in Grafana indicating that the license limit has been reached. Previously, all users saw the banner.
|
||||
|
||||
Grafana Enterprise license tokens update automatically on a daily basis, which means you no longer need to manually update your license, and the process for adding additional users to a license is smoother than it was before.
|
||||
|
||||
Refer to [Licensing restrictions]({{< relref "../enterprise/license/license-restrictions/" >}}) for more information.
|
||||
Refer to [Licensing restrictions]({{< relref "../administration/enterprise-licensing#license-restrictions" >}}) for more information.
|
||||
|
||||
### Export usage insights to Loki
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ Each Grafana Enterprise user will be limited to three concurrent user sessions.
|
||||
|
||||
A new session is created when you sign in to Grafana from a different device or a different browser. Multiple windows and tabs in the same browser are all part of the same session, so having many Grafana tabs open will not cause any issues.
|
||||
|
||||
For more information on Grafana Enterprise licensing and restrictions, refer to [License restrictions]({{< relref "../enterprise/license/license-restrictions/" >}}).
|
||||
For more information on Grafana Enterprise licensing and restrictions, refer to [License restrictions]({{< relref "../administration/enterprise-licensing#license-restrictions" >}}).
|
||||
|
||||
## Breaking changes
|
||||
|
||||
|
||||
@@ -156,7 +156,7 @@ Log navigation in Explore has been significantly improved. We added pagination t
|
||||
|
||||
You can now use the Plugin catalog app to easily manage your plugins from within Grafana. Install, update, and uninstall plugins without requiring a server restart.
|
||||
|
||||
[Plugin catalog]({{< relref "../plugins/catalog/" >}}) was added as a result of this feature.
|
||||
[Plugin catalog]({{< relref "../administration/plugin-management#plugin-catalog/" >}}) was added as a result of this feature.
|
||||
|
||||
### Performance improvements
|
||||
|
||||
@@ -294,7 +294,7 @@ These features are included in the Grafana Enterprise edition.
|
||||
|
||||
You can now add or remove detailed permissions from Viewer, Editor, and Admin org roles, to grant users just the right amount of access within Grafana. Available permissions include the ability to view and manage Users, Reports, and the Access Control API itself. Grafana will support more and more permissions over the coming months.
|
||||
|
||||
[Role-based access control docs]({{< relref "../enterprise/access-control/" >}}) were added as a result of this feature.
|
||||
[Role-based access control docs]({{< relref "../administration/roles-and-permissions/access-control/" >}}) were added as a result of this feature.
|
||||
|
||||
### Data source query caching
|
||||
|
||||
@@ -318,7 +318,7 @@ For more information, refer to [Reporting docs]({{< relref "../enterprise/report
|
||||
|
||||
The Grafana Enterprise documentation has been updated to describe more specifically how licensed roles are counted, how they can be updated, and where you can see details about dashboard and folder permissions that affect users' licensed roles.
|
||||
|
||||
For more information, refer to [License restrictions docs]({{< relref "../enterprise/license/license-restrictions/" >}}).
|
||||
For more information, refer to [License restrictions docs]({{< relref "../administration/enterprise-licensing#license-restrictions" >}}).
|
||||
|
||||
## Breaking changes
|
||||
|
||||
|
||||
@@ -146,7 +146,7 @@ These features are included in the Grafana Enterprise edition.
|
||||
|
||||
Role-based access control remains in beta. You can now grant or revoke permissions for Viewers, Editors, or Admins to use Explore mode, configure LDAP or SAML settings, or view the admin/stats page. These new permissions enhance the existing permissions that can be customized, namely permissions to access Users, Orgs, LDAP settings, and Reports in Grafana.
|
||||
|
||||
Fine grained access control allows you to customize roles and permissions in Grafana beyond the built-in Viewer, Editor, and Admin roles. As of 8.1, you can modify some of the permissions for any of these built-in roles. This is helpful if you’d like users to have more or fewer access permissions than a given role allows for by default. For an overview of role-based access control and a complete list of available permissions, refer to the [Fine grained access control]({{< relref "../enterprise/access-control/" >}}) documentation.
|
||||
Fine grained access control allows you to customize roles and permissions in Grafana beyond the built-in Viewer, Editor, and Admin roles. As of 8.1, you can modify some of the permissions for any of these built-in roles. This is helpful if you’d like users to have more or fewer access permissions than a given role allows for by default. For an overview of role-based access control and a complete list of available permissions, refer to the [Fine grained access control]({{< relref "../administration/roles-and-permissions/access-control/" >}}) documentation.
|
||||
|
||||
### New and improved reporting scheduler
|
||||
|
||||
|
||||
+2
-5
@@ -72,9 +72,6 @@
|
||||
],
|
||||
"*pkg/**/*.go": [
|
||||
"gofmt -w -s"
|
||||
],
|
||||
"*.star": [
|
||||
"make drone"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -259,7 +256,7 @@
|
||||
"@grafana/e2e-selectors": "workspace:*",
|
||||
"@grafana/experimental": "^0.0.2-canary.32",
|
||||
"@grafana/google-sdk": "0.0.3",
|
||||
"@grafana/lezer-logql": "^0.0.12",
|
||||
"@grafana/lezer-logql": "^0.0.13",
|
||||
"@grafana/runtime": "workspace:*",
|
||||
"@grafana/schema": "workspace:*",
|
||||
"@grafana/slate-react": "0.22.10-grafana",
|
||||
@@ -396,7 +393,7 @@
|
||||
"tether-drop": "https://github.com/torkelo/drop",
|
||||
"tinycolor2": "1.4.2",
|
||||
"tslib": "2.4.0",
|
||||
"uplot": "1.6.21",
|
||||
"uplot": "1.6.22",
|
||||
"uuid": "8.3.2",
|
||||
"vendor": "link:./public/vendor",
|
||||
"visjs-network": "4.25.0",
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
"regenerator-runtime": "0.13.9",
|
||||
"rxjs": "7.5.5",
|
||||
"tslib": "2.4.0",
|
||||
"uplot": "1.6.21",
|
||||
"uplot": "1.6.22",
|
||||
"xss": "1.0.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -241,10 +241,9 @@ export type TypedArray =
|
||||
| Float32Array
|
||||
| Float64Array;
|
||||
|
||||
export type AlignedData = [
|
||||
xValues: number[] | TypedArray,
|
||||
...yValues: Array<Array<number | null | undefined> | TypedArray>
|
||||
];
|
||||
export type AlignedData =
|
||||
| TypedArray[]
|
||||
| [xValues: number[] | TypedArray, ...yValues: Array<Array<number | null | undefined> | TypedArray>];
|
||||
|
||||
// nullModes
|
||||
const NULL_REMOVE = 0; // nulls are converted to undefined (e.g. for spanGaps: true)
|
||||
|
||||
@@ -13,9 +13,6 @@ describe('toUrlParams', () => {
|
||||
});
|
||||
expect(url).toBe('server=backend-01&hasSpace=has%20space&many=1&many=2&many=3&true&number=20&isNull=&isUndefined=');
|
||||
});
|
||||
});
|
||||
|
||||
describe('toUrlParams', () => {
|
||||
it('should encode the same way as angularjs', () => {
|
||||
const url = urlUtil.toUrlParams({
|
||||
server: ':@',
|
||||
@@ -30,6 +27,12 @@ describe('toUrlParams', () => {
|
||||
});
|
||||
expect(url).toBe('bool1&bool2=false');
|
||||
});
|
||||
it("should encode the following special characters [!'()*]", () => {
|
||||
const url = urlUtil.toUrlParams({
|
||||
datasource: "testDs[!'()*]",
|
||||
});
|
||||
expect(url).toBe('datasource=testDs%5B%21%27%28%29%2A%5D');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseKeyValue', () => {
|
||||
|
||||
@@ -32,7 +32,10 @@ function encodeURIComponentAsAngularJS(val: string, pctEncodeSpaces?: boolean) {
|
||||
.replace(/%24/g, '$')
|
||||
.replace(/%2C/gi, ',')
|
||||
.replace(/%3B/gi, ';')
|
||||
.replace(/%20/g, pctEncodeSpaces ? '%20' : '+');
|
||||
.replace(/%20/g, pctEncodeSpaces ? '%20' : '+')
|
||||
.replace(/[!'()*]/g, function (c) {
|
||||
return '%' + c.charCodeAt(0).toString(16).toUpperCase();
|
||||
});
|
||||
}
|
||||
|
||||
function toUrlParams(a: any) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
PUBLIC_DATASOURCE,
|
||||
PublicDashboardDataSource,
|
||||
} from '../../../../public/app/features/dashboard/services/PublicDashboardDataSource';
|
||||
import { MIXED_DATASOURCE_NAME } from '../../../../public/app/plugins/datasource/mixed/MixedDataSource';
|
||||
|
||||
import { DataSourceWithBackend } from './DataSourceWithBackend';
|
||||
|
||||
@@ -75,4 +76,26 @@ describe('PublicDashboardDatasource', () => {
|
||||
let ds = new PublicDashboardDataSource(datasource);
|
||||
expect(ds.uid).toBe('abc123');
|
||||
});
|
||||
|
||||
test('isMixedDatasource returns true when datasource is mixed', () => {
|
||||
const datasource = new DataSourceWithBackend({ id: 1, uid: MIXED_DATASOURCE_NAME } as DataSourceInstanceSettings);
|
||||
let ds = new PublicDashboardDataSource(datasource);
|
||||
expect(ds.meta.mixed).toBeTruthy();
|
||||
});
|
||||
|
||||
test('isMixedDatasource returns false when datasource is not mixed', () => {
|
||||
const datasource = new DataSourceWithBackend({ id: 1, uid: 'abc123' } as DataSourceInstanceSettings);
|
||||
let ds = new PublicDashboardDataSource(datasource);
|
||||
expect(ds.meta.mixed).toBeFalsy();
|
||||
});
|
||||
|
||||
test('isMixedDatasource returns false when datasource is a string', () => {
|
||||
let ds = new PublicDashboardDataSource('abc123');
|
||||
expect(ds.meta.mixed).toBeFalsy();
|
||||
});
|
||||
|
||||
test('isMixedDatasource returns false when datasource is null', () => {
|
||||
let ds = new PublicDashboardDataSource(null);
|
||||
expect(ds.meta.mixed).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
"slate-plain-serializer": "0.7.10",
|
||||
"tinycolor2": "1.4.2",
|
||||
"tslib": "2.4.0",
|
||||
"uplot": "1.6.21",
|
||||
"uplot": "1.6.22",
|
||||
"uuid": "8.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -110,6 +110,7 @@
|
||||
"@storybook/addons": "6.4.21",
|
||||
"@storybook/api": "6.4.21",
|
||||
"@storybook/builder-webpack5": "6.4.21",
|
||||
"@storybook/client-api": "6.4.21",
|
||||
"@storybook/components": "6.4.21",
|
||||
"@storybook/core-events": "6.4.21",
|
||||
"@storybook/manager-webpack5": "6.4.21",
|
||||
|
||||
@@ -62,7 +62,7 @@ export const DataLinksContextMenu: React.FC<DataLinksContextMenuProps> = ({ chil
|
||||
onClick={linkModel.onClick}
|
||||
target={linkModel.target}
|
||||
title={linkModel.title}
|
||||
style={style}
|
||||
style={{ ...style, overflow: 'hidden' }}
|
||||
aria-label={selectors.components.DataLinksContextMenu.singleLink}
|
||||
>
|
||||
{children({})}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { uniqueId, isString } from 'lodash';
|
||||
import { isString, uniqueId } from 'lodash';
|
||||
import React, { ReactNode, useCallback, useState } from 'react';
|
||||
import { DropEvent, DropzoneOptions, FileRejection, useDropzone, Accept } from 'react-dropzone';
|
||||
import { Accept, DropEvent, DropzoneOptions, FileRejection, useDropzone } from 'react-dropzone';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
|
||||
import { useTheme2 } from '../../themes';
|
||||
import { Alert } from '../Alert/Alert';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
|
||||
import { FileListItem } from './FileListItem';
|
||||
@@ -58,6 +59,7 @@ export interface DropzoneFile {
|
||||
|
||||
export function FileDropzone({ options, children, readAs, onLoad, fileListRenderer, onFileRemove }: FileDropzoneProps) {
|
||||
const [files, setFiles] = useState<DropzoneFile[]>([]);
|
||||
const [errorMessages, setErrorMessages] = useState<string[]>([]);
|
||||
|
||||
const setFileProperty = useCallback(
|
||||
(customFile: DropzoneFile, action: (customFileToModify: DropzoneFile) => void) => {
|
||||
@@ -83,6 +85,8 @@ export function FileDropzone({ options, children, readAs, onLoad, fileListRender
|
||||
setFiles((oldFiles) => [...oldFiles, ...customFiles]);
|
||||
}
|
||||
|
||||
setErrors(rejectedFiles);
|
||||
|
||||
if (options?.onDrop) {
|
||||
options.onDrop(acceptedFiles, rejectedFiles, event);
|
||||
} else {
|
||||
@@ -161,12 +165,40 @@ export function FileDropzone({ options, children, readAs, onLoad, fileListRender
|
||||
return <FileListItem key={file.id} file={file} removeFile={removeFile} />;
|
||||
});
|
||||
|
||||
const setErrors = (rejectedFiles: FileRejection[]) => {
|
||||
let errors: string[] = [];
|
||||
rejectedFiles.map((rejectedFile) => {
|
||||
rejectedFile.errors.map((error) => {
|
||||
errors.push(error.message);
|
||||
});
|
||||
});
|
||||
|
||||
setErrorMessages(errors);
|
||||
};
|
||||
|
||||
const getErrorMessages = () => {
|
||||
return (
|
||||
<div className={styles.errorAlert}>
|
||||
<Alert title="Upload failed" severity="error" onRemove={clearAlert}>
|
||||
{errorMessages.map((error) => {
|
||||
return <div key={error}>{error}</div>;
|
||||
})}
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const clearAlert = () => {
|
||||
setErrorMessages([]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div data-testid="dropzone" {...getRootProps({ className: styles.dropzone })}>
|
||||
<input {...getInputProps()} />
|
||||
{children ?? <FileDropzoneDefaultChildren primaryText={getPrimaryText(files, options)} />}
|
||||
</div>
|
||||
{errorMessages.length > 0 && getErrorMessages()}
|
||||
{options?.accept && (
|
||||
<small className={cx(styles.small, styles.acceptMargin)}>{getAcceptedFileTypeText(options.accept)}</small>
|
||||
)}
|
||||
@@ -276,5 +308,8 @@ function getStyles(theme: GrafanaTheme2, isDragActive?: boolean) {
|
||||
small: css`
|
||||
color: ${theme.colors.text.secondary};
|
||||
`,
|
||||
errorAlert: css`
|
||||
padding-top: 10px;
|
||||
`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Meta, Preview, Props } from '@storybook/addon-docs/blocks';
|
||||
import { RefreshPicker } from './RefreshPicker';
|
||||
|
||||
<Meta title="MDX|RefreshPicker" component={RefreshPicker} />
|
||||
|
||||
# Refresh Picker
|
||||
|
||||
This component is used on dashboards to refresh visualizations. Grafana does not do this automatically, queries run on their own schedule according to the panel settings. Grafana cancels any pending requests when a new refresh is triggered.
|
||||
|
||||
**The refresh icon:** will immediately run the query and refresh the visualizations.
|
||||
|
||||
**The down arrow:** will display a list of refresh intervals. If one of them is selected the dashboard will regularly refresh according to that schedule.
|
||||
|
||||
<Preview>
|
||||
<RefreshPicker tooltip="Run query" />
|
||||
<RefreshPicker text="Run query" tooltip="Run query" />
|
||||
<RefreshPicker text="Run query" isLoading={true} tooltip="Run query" value="1h" />
|
||||
</Preview>
|
||||
<Props of={RefreshPicker} />
|
||||
@@ -1,24 +1,45 @@
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { useArgs } from '@storybook/client-api';
|
||||
import { Story } from '@storybook/react';
|
||||
import React from 'react';
|
||||
|
||||
import { RefreshPicker } from '@grafana/ui';
|
||||
|
||||
import { DashboardStoryCanvas } from '../../utils/storybook/DashboardStoryCanvas';
|
||||
import { StoryExample } from '../../utils/storybook/StoryExample';
|
||||
import { UseState } from '../../utils/storybook/UseState';
|
||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||
import { HorizontalGroup } from '../Layout/Layout';
|
||||
|
||||
import { Props } from './RefreshPicker';
|
||||
import mdx from './RefreshPicker.mdx';
|
||||
|
||||
export default {
|
||||
title: 'Pickers and Editors/RefreshPicker',
|
||||
component: RefreshPicker,
|
||||
decorators: [withCenteredStory],
|
||||
parameters: {
|
||||
docs: {
|
||||
page: mdx,
|
||||
},
|
||||
controls: {
|
||||
sort: 'alpha',
|
||||
},
|
||||
},
|
||||
args: {
|
||||
isLoading: false,
|
||||
isLive: false,
|
||||
width: 'auto',
|
||||
text: 'Run query',
|
||||
tooltip: 'My tooltip text goes here',
|
||||
value: '1h',
|
||||
primary: false,
|
||||
noIntervalPicker: false,
|
||||
intervals: ['5s', '10s', '30s', '1m', '5m', '15m', '30m', '1h', '2h', '1d'],
|
||||
},
|
||||
};
|
||||
|
||||
export const Examples = () => {
|
||||
const intervals = ['5s', '10s', '30s', '1m', '5m', '15m', '30m', '1h', '2h', '1d'];
|
||||
export const Examples: Story<Props> = (args) => {
|
||||
const [, updateArgs] = useArgs();
|
||||
const onIntervalChanged = (interval: string) => {
|
||||
action('onIntervalChanged fired')(interval);
|
||||
updateArgs({ value: interval });
|
||||
};
|
||||
|
||||
const onRefresh = () => {
|
||||
@@ -26,45 +47,17 @@ export const Examples = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<DashboardStoryCanvas>
|
||||
<UseState initialState={'1h'}>
|
||||
{(value, updateValue) => {
|
||||
return (
|
||||
<HorizontalGroup>
|
||||
<StoryExample name="Simple">
|
||||
<RefreshPicker
|
||||
tooltip="Hello world"
|
||||
value={value}
|
||||
intervals={intervals}
|
||||
onIntervalChanged={onIntervalChanged}
|
||||
onRefresh={onRefresh}
|
||||
/>
|
||||
</StoryExample>
|
||||
<StoryExample name="With text">
|
||||
<RefreshPicker
|
||||
tooltip="Hello world"
|
||||
value={value}
|
||||
text="Run query"
|
||||
intervals={intervals}
|
||||
onIntervalChanged={onIntervalChanged}
|
||||
onRefresh={onRefresh}
|
||||
/>
|
||||
</StoryExample>
|
||||
<StoryExample name="With text and loading">
|
||||
<RefreshPicker
|
||||
tooltip="Hello world"
|
||||
value={value}
|
||||
text="Run query"
|
||||
isLoading={true}
|
||||
intervals={intervals}
|
||||
onIntervalChanged={onIntervalChanged}
|
||||
onRefresh={onRefresh}
|
||||
/>
|
||||
</StoryExample>
|
||||
</HorizontalGroup>
|
||||
);
|
||||
}}
|
||||
</UseState>
|
||||
</DashboardStoryCanvas>
|
||||
<RefreshPicker
|
||||
tooltip={args.tooltip}
|
||||
value={args.value}
|
||||
text={args.text}
|
||||
isLoading={args.isLoading}
|
||||
intervals={args.intervals}
|
||||
width={args.width}
|
||||
onIntervalChanged={onIntervalChanged}
|
||||
onRefresh={onRefresh}
|
||||
noIntervalPicker={args.noIntervalPicker}
|
||||
primary={args.primary}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -257,13 +257,14 @@ export const Table: FC<Props> = memo((props: Props) => {
|
||||
if (enablePagination) {
|
||||
const itemsRangeStart = state.pageIndex * state.pageSize + 1;
|
||||
let itemsRangeEnd = itemsRangeStart + state.pageSize - 1;
|
||||
const isSmall = width < 500;
|
||||
const isSmall = width < 550;
|
||||
if (itemsRangeEnd > data.length) {
|
||||
itemsRangeEnd = data.length;
|
||||
}
|
||||
paginationEl = (
|
||||
<div className={tableStyles.paginationWrapper}>
|
||||
<div>
|
||||
{isSmall ? null : <div className={tableStyles.paginationItem} />}
|
||||
<div className={tableStyles.paginationCenterItem}>
|
||||
<Pagination
|
||||
currentPage={state.pageIndex + 1}
|
||||
numberOfPages={pageOptions.length}
|
||||
|
||||
@@ -106,6 +106,7 @@ export const getTableStyles = (theme: GrafanaTheme2) => {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`,
|
||||
thead: css`
|
||||
label: thead;
|
||||
@@ -182,17 +183,22 @@ export const getTableStyles = (theme: GrafanaTheme2) => {
|
||||
li {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
div:not(:only-child):first-child {
|
||||
flex-grow: 0.6;
|
||||
}
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
`,
|
||||
paginationItem: css`
|
||||
flex: 20%;
|
||||
`,
|
||||
paginationCenterItem: css`
|
||||
flex: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
`,
|
||||
paginationSummary: css`
|
||||
color: ${theme.colors.text.secondary};
|
||||
font-size: ${theme.typography.bodySmall.fontSize};
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex: 20%;
|
||||
padding-right: ${theme.spacing(1)};
|
||||
`,
|
||||
|
||||
tableContentWrapper: (totalColumnsWidth: number) => {
|
||||
|
||||
@@ -102,14 +102,6 @@ export class UPlotChart extends React.Component<PlotProps, UPlotChartState> {
|
||||
this.reinitPlot();
|
||||
} else if (!sameData(prevProps, this.props)) {
|
||||
plot?.setData(this.props.data as AlignedData);
|
||||
|
||||
// this is a uPlot cache-busting hack for bar charts in case x axis labels changed
|
||||
// since the x scale's "range" doesnt change, the axis size doesnt get recomputed, which is where the tick labels are regenerated & cached
|
||||
// the more expensive, more proper/thorough way to do this is to force all axes to recalc: plot?.redraw(false, true);
|
||||
if (plot && typeof this.props.data[0]?.[0] === 'string') {
|
||||
//@ts-ignore
|
||||
plot.axes[0]._values = this.props.data[0];
|
||||
}
|
||||
} else if (!sameTimeRange(prevProps, this.props)) {
|
||||
plot?.setScale('x', {
|
||||
min: this.props.timeRange.from.valueOf(),
|
||||
|
||||
@@ -2,6 +2,7 @@ package api
|
||||
|
||||
import (
|
||||
"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/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
@@ -36,6 +37,11 @@ var (
|
||||
// grants to organization roles ("Viewer", "Editor", "Admin") or "Grafana Admin"
|
||||
// that HTTPServer needs
|
||||
func (hs *HTTPServer) declareFixedRoles() error {
|
||||
// Declare plugins roles
|
||||
if err := plugins.DeclareRBACRoles(hs.AccessControl); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
provisioningWriterRole := ac.RoleRegistration{
|
||||
Role: ac.RoleDTO{
|
||||
Name: "fixed:provisioning:writer",
|
||||
@@ -423,6 +429,13 @@ var orgsCreateAccessEvaluator = ac.EvalAll(
|
||||
ac.EvalPermission(ActionOrgsCreate),
|
||||
)
|
||||
|
||||
// usersInviteEvaluator is used to protect the "Configuration > Users > Invite" page access
|
||||
// accessible to org admins and server admins by default
|
||||
var usersInviteEvaluator = ac.EvalAny(
|
||||
ac.EvalPermission(ac.ActionUsersCreate),
|
||||
ac.EvalPermission(ac.ActionOrgUsersAdd),
|
||||
)
|
||||
|
||||
// teamsAccessEvaluator is used to protect the "Configuration > Teams" page access
|
||||
// grants access to a user when they can either create teams or can read and update a team
|
||||
var teamsAccessEvaluator = ac.EvalAny(
|
||||
@@ -450,7 +463,10 @@ var teamsEditAccessEvaluator = ac.EvalAll(
|
||||
var apiKeyAccessEvaluator = ac.EvalPermission(ac.ActionAPIKeyRead)
|
||||
|
||||
// serviceAccountAccessEvaluator is used to protect the "Configuration > Service accounts" page access
|
||||
var serviceAccountAccessEvaluator = ac.EvalPermission(serviceaccounts.ActionRead)
|
||||
var serviceAccountAccessEvaluator = ac.EvalAny(
|
||||
ac.EvalPermission(serviceaccounts.ActionRead),
|
||||
ac.EvalPermission(serviceaccounts.ActionCreate),
|
||||
)
|
||||
|
||||
// Metadata helpers
|
||||
// getAccessControlMetadata returns the accesscontrol metadata associated with a given resource
|
||||
|
||||
+16
-9
@@ -9,6 +9,7 @@ import (
|
||||
"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/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
@@ -59,7 +60,7 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
r.Get("/datasources/edit/*", authorize(reqOrgAdmin, datasources.EditPageAccess), hs.Index)
|
||||
r.Get("/org/users", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersRead)), hs.Index)
|
||||
r.Get("/org/users/new", reqOrgAdmin, hs.Index)
|
||||
r.Get("/org/users/invite", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionUsersCreate)), hs.Index)
|
||||
r.Get("/org/users/invite", authorize(reqOrgAdmin, usersInviteEvaluator), hs.Index)
|
||||
r.Get("/org/teams", authorize(reqCanAccessTeams, ac.EvalPermission(ac.ActionTeamsRead)), hs.Index)
|
||||
r.Get("/org/teams/edit/*", authorize(reqCanAccessTeams, teamsEditAccessEvaluator), hs.Index)
|
||||
r.Get("/org/teams/new", authorize(reqCanAccessTeams, ac.EvalPermission(ac.ActionTeamsCreate)), hs.Index)
|
||||
@@ -88,8 +89,10 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
r.Get("/plugins/:id/", reqSignedIn, hs.Index)
|
||||
r.Get("/plugins/:id/edit", reqSignedIn, hs.Index) // deprecated
|
||||
r.Get("/plugins/:id/page/:page", reqSignedIn, hs.Index)
|
||||
r.Get("/a/:id/*", reqSignedIn, hs.Index) // App Root Page
|
||||
r.Get("/a/:id", reqSignedIn, hs.Index)
|
||||
// App Root Page
|
||||
appPluginIDScope := plugins.ScopeProvider.GetResourceScope(":id")
|
||||
r.Get("/a/:id/*", authorize(reqSignedIn, ac.EvalPermission(plugins.ActionAppAccess, appPluginIDScope)), hs.Index)
|
||||
r.Get("/a/:id", authorize(reqSignedIn, ac.EvalPermission(plugins.ActionAppAccess, appPluginIDScope)), hs.Index)
|
||||
|
||||
r.Get("/d/:uid/:slug", reqSignedIn, redirectFromLegacyPanelEditURL, hs.Index)
|
||||
r.Get("/d/:uid", reqSignedIn, redirectFromLegacyPanelEditURL, hs.Index)
|
||||
@@ -225,8 +228,10 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
orgRoute.Get("/read/*", routing.Wrap(hs.StorageService.Read))
|
||||
|
||||
if hs.Features.IsEnabled(featuremgmt.FlagStorageLocalUpload) {
|
||||
orgRoute.Delete("/delete/*", reqSignedIn, routing.Wrap(hs.StorageService.Delete))
|
||||
orgRoute.Post("/upload", reqSignedIn, routing.Wrap(hs.StorageService.Upload))
|
||||
orgRoute.Post("/delete/*", reqGrafanaAdmin, routing.Wrap(hs.StorageService.Delete))
|
||||
orgRoute.Post("/upload", reqGrafanaAdmin, routing.Wrap(hs.StorageService.Upload))
|
||||
orgRoute.Post("/createFolder", reqGrafanaAdmin, routing.Wrap(hs.StorageService.CreateFolder))
|
||||
orgRoute.Post("/deleteFolder", reqGrafanaAdmin, routing.Wrap(hs.StorageService.DeleteFolder))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -244,7 +249,7 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
|
||||
// invites
|
||||
orgRoute.Get("/invites", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionUsersCreate)), routing.Wrap(hs.GetPendingOrgInvites))
|
||||
orgRoute.Post("/invites", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionUsersCreate)), quota("user"), routing.Wrap(hs.AddOrgInvite))
|
||||
orgRoute.Post("/invites", authorize(reqOrgAdmin, usersInviteEvaluator), quota("user"), routing.Wrap(hs.AddOrgInvite))
|
||||
orgRoute.Patch("/invites/:code/revoke", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionUsersCreate)), routing.Wrap(hs.RevokeInvite))
|
||||
|
||||
// prefs
|
||||
@@ -325,12 +330,13 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
datasourceRoute.Get("/id/:name", authorize(reqSignedIn, ac.EvalPermission(datasources.ActionIDRead, nameScope)), routing.Wrap(hs.GetDataSourceIdByName))
|
||||
})
|
||||
|
||||
pluginIDScope := plugins.ScopeProvider.GetResourceScope(":pluginId")
|
||||
apiRoute.Get("/plugins", routing.Wrap(hs.GetPluginList))
|
||||
apiRoute.Get("/plugins/:pluginId/settings", routing.Wrap(hs.GetPluginSettingByID))
|
||||
apiRoute.Get("/plugins/:pluginId/settings", routing.Wrap(hs.GetPluginSettingByID)) // RBAC check performed in handler for App Plugins
|
||||
apiRoute.Get("/plugins/:pluginId/markdown/:name", routing.Wrap(hs.GetPluginMarkdown))
|
||||
apiRoute.Get("/plugins/:pluginId/health", routing.Wrap(hs.CheckHealth))
|
||||
apiRoute.Any("/plugins/:pluginId/resources", hs.CallResource)
|
||||
apiRoute.Any("/plugins/:pluginId/resources/*", hs.CallResource)
|
||||
apiRoute.Any("/plugins/:pluginId/resources", authorize(reqSignedIn, ac.EvalPermission(plugins.ActionAppAccess, pluginIDScope)), hs.CallResource)
|
||||
apiRoute.Any("/plugins/:pluginId/resources/*", authorize(reqSignedIn, ac.EvalPermission(plugins.ActionAppAccess, pluginIDScope)), hs.CallResource)
|
||||
apiRoute.Get("/plugins/errors", routing.Wrap(hs.GetPluginErrorsList))
|
||||
|
||||
if hs.Cfg.PluginAdminEnabled && !hs.Cfg.PluginAdminExternalManageEnabled {
|
||||
@@ -559,6 +565,7 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
if hs.Features.IsEnabled(featuremgmt.FlagExport) {
|
||||
adminRoute.Get("/export", reqGrafanaAdmin, routing.Wrap(hs.ExportService.HandleGetStatus))
|
||||
adminRoute.Post("/export", reqGrafanaAdmin, routing.Wrap(hs.ExportService.HandleRequestExport))
|
||||
adminRoute.Post("/export/stop", reqGrafanaAdmin, routing.Wrap(hs.ExportService.HandleRequestStop))
|
||||
}
|
||||
|
||||
adminRoute.Post("/encryption/rotate-data-keys", reqGrafanaAdmin, routing.Wrap(hs.AdminRotateDataEncryptionKeys))
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"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/util"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
@@ -42,6 +43,11 @@ func (hs *HTTPServer) initAppPluginRoutes(r *web.Mux) {
|
||||
ReqSignedIn: true,
|
||||
}))
|
||||
|
||||
// Preventing access to plugin routes if the user has no right to access the plugin
|
||||
authorize := ac.Middleware(hs.AccessControl)
|
||||
handlers = append(handlers, authorize(middleware.ReqSignedIn,
|
||||
ac.EvalPermission(plugins.ActionAppAccess, plugins.ScopeProvider.GetResourceScope(plugin.ID))))
|
||||
|
||||
if route.ReqRole != "" {
|
||||
if route.ReqRole == models.ROLE_ADMIN {
|
||||
handlers = append(handlers, middleware.RoleAuth(models.ROLE_ADMIN))
|
||||
@@ -49,6 +55,7 @@ func (hs *HTTPServer) initAppPluginRoutes(r *web.Mux) {
|
||||
handlers = append(handlers, middleware.RoleAuth(models.ROLE_EDITOR, models.ROLE_ADMIN))
|
||||
}
|
||||
}
|
||||
|
||||
handlers = append(handlers, AppPluginRoute(route, plugin.ID, hs))
|
||||
for _, method := range strings.Split(route.Method, ",") {
|
||||
r.Handle(strings.TrimSpace(method), url, handlers)
|
||||
|
||||
@@ -268,24 +268,28 @@ func (hs *HTTPServer) DeleteDashboardSnapshot(c *models.ReqContext) response.Res
|
||||
return response.Error(404, "Failed to get dashboard snapshot", nil)
|
||||
}
|
||||
|
||||
dashboardID := query.Result.Dashboard.Get("id").MustInt64()
|
||||
|
||||
guardian := guardian.New(c.Req.Context(), dashboardID, c.OrgId, c.SignedInUser)
|
||||
canEdit, err := guardian.CanEdit()
|
||||
// check for permissions only if the dahboard is found
|
||||
if err != nil && !errors.Is(err, dashboards.ErrDashboardNotFound) {
|
||||
return response.Error(500, "Error while checking permissions for snapshot", err)
|
||||
}
|
||||
|
||||
if !canEdit && query.Result.UserId != c.SignedInUser.UserId && !errors.Is(err, dashboards.ErrDashboardNotFound) {
|
||||
return response.Error(403, "Access denied to this snapshot", nil)
|
||||
}
|
||||
|
||||
if query.Result.External {
|
||||
err := deleteExternalDashboardSnapshot(query.Result.ExternalDeleteUrl)
|
||||
if err != nil {
|
||||
return response.Error(500, "Failed to delete external dashboard", err)
|
||||
}
|
||||
} else {
|
||||
// When creating an external snapshot, its dashboard content is empty. This means that the mustInt here returns a 0,
|
||||
// which before RBAC would result in a dashboard which has no ACL. A dashboard without an ACL would fallback
|
||||
// to the user’s org role, which for editors and admins would essentially always be allowed here. With RBAC,
|
||||
// all permissions must be explicit, so the lack of a rule for dashboard 0 means the guardian will reject.
|
||||
dashboardID := query.Result.Dashboard.Get("id").MustInt64()
|
||||
|
||||
guardian := guardian.New(c.Req.Context(), dashboardID, c.OrgId, c.SignedInUser)
|
||||
canEdit, err := guardian.CanEdit()
|
||||
// check for permissions only if the dahboard is found
|
||||
if err != nil && !errors.Is(err, dashboards.ErrDashboardNotFound) {
|
||||
return response.Error(500, "Error while checking permissions for snapshot", err)
|
||||
}
|
||||
|
||||
if !canEdit && query.Result.UserId != c.SignedInUser.UserId && !errors.Is(err, dashboards.ErrDashboardNotFound) {
|
||||
return response.Error(403, "Access denied to this snapshot", nil)
|
||||
}
|
||||
}
|
||||
|
||||
cmd := &dashboardsnapshots.DeleteDashboardSnapshotCommand{DeleteKey: query.Result.DeleteKey}
|
||||
|
||||
@@ -65,11 +65,7 @@ func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) {
|
||||
t.Run("When user has editor role and is not in the ACL", func(t *testing.T) {
|
||||
loggedInUserScenarioWithRole(t, "Should not be able to delete snapshot when calling DELETE on",
|
||||
"DELETE", "/api/snapshots/12345", "/api/snapshots/:key", models.ROLE_EDITOR, func(sc *scenarioContext) {
|
||||
var externalRequest *http.Request
|
||||
ts := setupRemoteServer(func(rw http.ResponseWriter, req *http.Request) {
|
||||
externalRequest = req
|
||||
})
|
||||
hs := &HTTPServer{dashboardsnapshotsService: setUpSnapshotTest(t, 0, ts.URL)}
|
||||
hs := &HTTPServer{dashboardsnapshotsService: setUpSnapshotTest(t, 0, "")}
|
||||
sc.handlerFunc = hs.DeleteDashboardSnapshot
|
||||
|
||||
dashSvc := dashboards.NewFakeDashboardService(t)
|
||||
@@ -79,7 +75,6 @@ func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) {
|
||||
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"key": "12345"}).exec()
|
||||
|
||||
assert.Equal(t, 403, sc.resp.Code)
|
||||
require.Nil(t, externalRequest)
|
||||
}, sqlmock)
|
||||
})
|
||||
|
||||
|
||||
@@ -11,10 +11,10 @@ import (
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/go-kit/log"
|
||||
"github.com/go-kit/log/level"
|
||||
"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/infra/log/level"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@@ -258,7 +258,7 @@ func TestFrontendLoggingEndpointSentry(t *testing.T) {
|
||||
assert.Len(t, logs, 10)
|
||||
assertContextContains(t, logs, "logger", "frontend")
|
||||
assertContextContains(t, logs, "msg", "hello world")
|
||||
assertContextContains(t, logs, "lvl", level.InfoValue())
|
||||
assertContextContains(t, logs, level.Key().(string), level.InfoValue())
|
||||
assertContextContains(t, logs, "logger", "frontend")
|
||||
assertContextContains(t, logs, "url", messageEvent.Request.URL)
|
||||
assertContextContains(t, logs, "user_agent", messageEvent.Request.Headers["User-Agent"])
|
||||
|
||||
@@ -76,6 +76,7 @@ func (hs *HTTPServer) getProfileNode(c *models.ReqContext) *dtos.NavLink {
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) getAppLinks(c *models.ReqContext) ([]*dtos.NavLink, error) {
|
||||
hasAccess := ac.HasAccess(hs.AccessControl, c)
|
||||
enabledPlugins, err := hs.enabledPlugins(c.Req.Context(), c.OrgId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -87,6 +88,11 @@ func (hs *HTTPServer) getAppLinks(c *models.ReqContext) ([]*dtos.NavLink, error)
|
||||
continue
|
||||
}
|
||||
|
||||
if !hasAccess(ac.ReqSignedIn,
|
||||
ac.EvalPermission(plugins.ActionAppAccess, plugins.ScopeProvider.GetResourceScope(plugin.ID))) {
|
||||
continue
|
||||
}
|
||||
|
||||
appLink := &dtos.NavLink{
|
||||
Text: plugin.Name,
|
||||
Id: "plugin-page-" + plugin.ID,
|
||||
|
||||
+1
-1
@@ -124,7 +124,7 @@ func (hs *HTTPServer) LoginView(c *models.ReqContext) {
|
||||
user := &user.User{ID: c.SignedInUser.UserId, Email: c.SignedInUser.Email, Login: c.SignedInUser.Login}
|
||||
err := hs.loginUserWithUser(user, c)
|
||||
if err != nil {
|
||||
c.Handle(hs.Cfg, 500, "Failed to sign in user", err)
|
||||
c.Handle(hs.Cfg, http.StatusInternalServerError, "Failed to sign in user", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
+14
-14
@@ -34,10 +34,10 @@ func (hs *HTTPServer) GetOrgByName(c *models.ReqContext) response.Response {
|
||||
org, err := hs.SQLStore.GetOrgByName(web.Params(c.Req)[":name"])
|
||||
if err != nil {
|
||||
if errors.Is(err, models.ErrOrgNotFound) {
|
||||
return response.Error(404, "Organization not found", err)
|
||||
return response.Error(http.StatusNotFound, "Organization not found", err)
|
||||
}
|
||||
|
||||
return response.Error(500, "Failed to get organization", err)
|
||||
return response.Error(http.StatusInternalServerError, "Failed to get organization", err)
|
||||
}
|
||||
result := models.OrgDetailsDTO{
|
||||
Id: org.Id,
|
||||
@@ -60,9 +60,9 @@ func (hs *HTTPServer) getOrgHelper(ctx context.Context, orgID int64) response.Re
|
||||
|
||||
if err := hs.SQLStore.GetOrgById(ctx, &query); err != nil {
|
||||
if errors.Is(err, models.ErrOrgNotFound) {
|
||||
return response.Error(404, "Organization not found", err)
|
||||
return response.Error(http.StatusNotFound, "Organization not found", err)
|
||||
}
|
||||
return response.Error(500, "Failed to get organization", err)
|
||||
return response.Error(http.StatusInternalServerError, "Failed to get organization", err)
|
||||
}
|
||||
|
||||
org := query.Result
|
||||
@@ -90,15 +90,15 @@ func (hs *HTTPServer) CreateOrg(c *models.ReqContext) response.Response {
|
||||
}
|
||||
acEnabled := !hs.AccessControl.IsDisabled()
|
||||
if !acEnabled && !(setting.AllowUserOrgCreate || c.IsGrafanaAdmin) {
|
||||
return response.Error(403, "Access denied", nil)
|
||||
return response.Error(http.StatusForbidden, "Access denied", nil)
|
||||
}
|
||||
|
||||
cmd.UserId = c.UserId
|
||||
if err := hs.SQLStore.CreateOrg(c.Req.Context(), &cmd); err != nil {
|
||||
if errors.Is(err, models.ErrOrgNameTaken) {
|
||||
return response.Error(409, "Organization name taken", err)
|
||||
return response.Error(http.StatusConflict, "Organization name taken", err)
|
||||
}
|
||||
return response.Error(500, "Failed to create organization", err)
|
||||
return response.Error(http.StatusInternalServerError, "Failed to create organization", err)
|
||||
}
|
||||
|
||||
metrics.MApiOrgCreate.Inc()
|
||||
@@ -135,9 +135,9 @@ func (hs *HTTPServer) updateOrgHelper(ctx context.Context, form dtos.UpdateOrgFo
|
||||
cmd := models.UpdateOrgCommand{Name: form.Name, OrgId: orgID}
|
||||
if err := hs.SQLStore.UpdateOrg(ctx, &cmd); err != nil {
|
||||
if errors.Is(err, models.ErrOrgNameTaken) {
|
||||
return response.Error(400, "Organization name taken", err)
|
||||
return response.Error(http.StatusBadRequest, "Organization name taken", err)
|
||||
}
|
||||
return response.Error(500, "Failed to update organization", err)
|
||||
return response.Error(http.StatusInternalServerError, "Failed to update organization", err)
|
||||
}
|
||||
|
||||
return response.Success("Organization updated")
|
||||
@@ -179,7 +179,7 @@ func (hs *HTTPServer) updateOrgAddressHelper(ctx context.Context, form dtos.Upda
|
||||
}
|
||||
|
||||
if err := hs.SQLStore.UpdateOrgAddress(ctx, &cmd); err != nil {
|
||||
return response.Error(500, "Failed to update org address", err)
|
||||
return response.Error(http.StatusInternalServerError, "Failed to update org address", err)
|
||||
}
|
||||
|
||||
return response.Success("Address updated")
|
||||
@@ -193,14 +193,14 @@ func (hs *HTTPServer) DeleteOrgByID(c *models.ReqContext) response.Response {
|
||||
}
|
||||
// before deleting an org, check if user does not belong to the current org
|
||||
if c.OrgId == orgID {
|
||||
return response.Error(400, "Can not delete org for current user", nil)
|
||||
return response.Error(http.StatusBadRequest, "Can not delete org for current user", nil)
|
||||
}
|
||||
|
||||
if err := hs.SQLStore.DeleteOrg(c.Req.Context(), &models.DeleteOrgCommand{Id: orgID}); err != nil {
|
||||
if errors.Is(err, models.ErrOrgNotFound) {
|
||||
return response.Error(404, "Failed to delete organization. ID not found", nil)
|
||||
return response.Error(http.StatusNotFound, "Failed to delete organization. ID not found", nil)
|
||||
}
|
||||
return response.Error(500, "Failed to update organization", err)
|
||||
return response.Error(http.StatusInternalServerError, "Failed to update organization", err)
|
||||
}
|
||||
return response.Success("Organization deleted")
|
||||
}
|
||||
@@ -221,7 +221,7 @@ func (hs *HTTPServer) SearchOrgs(c *models.ReqContext) response.Response {
|
||||
}
|
||||
|
||||
if err := hs.SQLStore.SearchOrgs(c.Req.Context(), &query); err != nil {
|
||||
return response.Error(500, "Failed to search orgs", err)
|
||||
return response.Error(http.StatusInternalServerError, "Failed to search orgs", err)
|
||||
}
|
||||
|
||||
return response.JSON(http.StatusOK, query.Result)
|
||||
|
||||
+20
-1
@@ -5,12 +5,14 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"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"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
@@ -50,9 +52,27 @@ func (hs *HTTPServer) AddOrgInvite(c *models.ReqContext) response.Response {
|
||||
return response.Error(500, "Failed to query db for existing user check", err)
|
||||
}
|
||||
} else {
|
||||
// Evaluate permissions for adding an existing user to the organization
|
||||
userIDScope := ac.Scope("users", "id", strconv.Itoa(int(userQuery.Result.ID)))
|
||||
hasAccess, err := hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, ac.EvalPermission(ac.ActionOrgUsersAdd, userIDScope))
|
||||
if err != nil {
|
||||
return response.Error(http.StatusInternalServerError, "Failed to evaluate permissions", err)
|
||||
}
|
||||
if !hasAccess {
|
||||
return response.Error(http.StatusForbidden, "Permission denied: not permitted to add an existing user to this organisation", err)
|
||||
}
|
||||
return hs.inviteExistingUserToOrg(c, userQuery.Result, &inviteDto)
|
||||
}
|
||||
|
||||
// Evaluate permissions for inviting a new user to Grafana
|
||||
hasAccess, err := hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, ac.EvalPermission(ac.ActionUsersCreate))
|
||||
if err != nil {
|
||||
return response.Error(http.StatusInternalServerError, "Failed to evaluate permissions", err)
|
||||
}
|
||||
if !hasAccess {
|
||||
return response.Error(http.StatusForbidden, "Permission denied: not permitted to create a new user", err)
|
||||
}
|
||||
|
||||
if setting.DisableLoginForm {
|
||||
return response.Error(400, "Cannot invite when login is disabled.", nil)
|
||||
}
|
||||
@@ -63,7 +83,6 @@ func (hs *HTTPServer) AddOrgInvite(c *models.ReqContext) response.Response {
|
||||
cmd.Name = inviteDto.Name
|
||||
cmd.Status = models.TmpUserInvitePending
|
||||
cmd.InvitedByUserId = c.UserId
|
||||
var err error
|
||||
cmd.Code, err = util.GetRandomString(30)
|
||||
if err != nil {
|
||||
return response.Error(500, "Could not generate random string", err)
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
)
|
||||
|
||||
func TestOrgInvitesAPIEndpointAccess(t *testing.T) {
|
||||
type accessControlTestCase2 struct {
|
||||
expectedCode int
|
||||
desc string
|
||||
url string
|
||||
method string
|
||||
permissions []accesscontrol.Permission
|
||||
input string
|
||||
}
|
||||
tests := []accessControlTestCase2{
|
||||
{
|
||||
expectedCode: http.StatusOK,
|
||||
desc: "org viewer with the correct permissions can invite and existing user to his org",
|
||||
url: "/api/org/invites",
|
||||
method: http.MethodPost,
|
||||
permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionOrgUsersAdd, Scope: accesscontrol.ScopeUsersAll}},
|
||||
input: `{"loginOrEmail": "` + testAdminOrg2.Login + `", "role": "` + string(models.ROLE_VIEWER) + `"}`,
|
||||
},
|
||||
{
|
||||
expectedCode: http.StatusForbidden,
|
||||
desc: "org viewer with missing permissions cannot invite and existing user to his org",
|
||||
url: "/api/org/invites",
|
||||
method: http.MethodPost,
|
||||
permissions: []accesscontrol.Permission{},
|
||||
input: `{"loginOrEmail": "` + testAdminOrg2.Login + `", "role": "` + string(models.ROLE_VIEWER) + `"}`,
|
||||
},
|
||||
{
|
||||
expectedCode: http.StatusForbidden,
|
||||
desc: "org viewer with the wrong scope cannot invite and existing user to his org",
|
||||
url: "/api/org/invites",
|
||||
method: http.MethodPost,
|
||||
permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionOrgUsersAdd, Scope: "users:id:100"}},
|
||||
input: `{"loginOrEmail": "` + testAdminOrg2.Login + `", "role": "` + string(models.ROLE_VIEWER) + `"}`,
|
||||
},
|
||||
{
|
||||
expectedCode: http.StatusForbidden,
|
||||
desc: "org viewer with user add permission cannot invite a new user to his org",
|
||||
url: "/api/org/invites",
|
||||
method: http.MethodPost,
|
||||
permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionOrgUsersAdd, Scope: accesscontrol.ScopeUsersAll}},
|
||||
input: `{"loginOrEmail": "new user", "role": "` + string(models.ROLE_VIEWER) + `"}`,
|
||||
},
|
||||
{
|
||||
expectedCode: http.StatusOK,
|
||||
desc: "org viewer with the correct permissions can invite a new user to his org",
|
||||
url: "/api/org/invites",
|
||||
method: http.MethodPost,
|
||||
permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionUsersCreate}},
|
||||
input: `{"loginOrEmail": "new user", "role": "` + string(models.ROLE_VIEWER) + `"}`,
|
||||
},
|
||||
{
|
||||
expectedCode: http.StatusForbidden,
|
||||
desc: "org viewer with missing permissions cannot invite a new user to his org",
|
||||
url: "/api/org/invites",
|
||||
method: http.MethodPost,
|
||||
permissions: []accesscontrol.Permission{},
|
||||
input: `{"loginOrEmail": "new user", "role": "` + string(models.ROLE_VIEWER) + `"}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
sc := setupHTTPServer(t, true, true)
|
||||
setInitCtxSignedInViewer(sc.initCtx)
|
||||
setupOrgUsersDBForAccessControlTests(t, sc.db)
|
||||
setAccessControlPermissions(sc.acmock, test.permissions, sc.initCtx.OrgId)
|
||||
|
||||
input := strings.NewReader(test.input)
|
||||
response := callAPI(sc.server, test.method, test.url, input, t)
|
||||
assert.Equal(t, test.expectedCode, response.Code)
|
||||
})
|
||||
}
|
||||
}
|
||||
+11
-1
@@ -119,7 +119,17 @@ func (hs *HTTPServer) GetPluginSettingByID(c *models.ReqContext) response.Respon
|
||||
|
||||
plugin, exists := hs.pluginStore.Plugin(c.Req.Context(), pluginID)
|
||||
if !exists {
|
||||
return response.Error(404, "Plugin not found, no installed plugin with that id", nil)
|
||||
return response.Error(http.StatusNotFound, "Plugin not found, no installed plugin with that id", nil)
|
||||
}
|
||||
|
||||
// In a first iteration, we only have one permission for app plugins.
|
||||
// We will need a different permission to allow users to configure the plugin without needing access to it.
|
||||
if plugin.IsApp() {
|
||||
hasAccess := accesscontrol.HasAccess(hs.AccessControl, c)
|
||||
if !hasAccess(accesscontrol.ReqSignedIn,
|
||||
accesscontrol.EvalPermission(plugins.ActionAppAccess, plugins.ScopeProvider.GetResourceScope(plugin.ID))) {
|
||||
return response.Error(http.StatusForbidden, "Access Denied", nil)
|
||||
}
|
||||
}
|
||||
|
||||
dto := &dtos.PluginSetting{
|
||||
|
||||
@@ -27,7 +27,7 @@ var (
|
||||
)
|
||||
|
||||
func ValidatePath(path string) error {
|
||||
if !filepath.IsAbs(path) {
|
||||
if !strings.HasPrefix(path, Delimiter) {
|
||||
return ErrRelativePath
|
||||
}
|
||||
|
||||
@@ -39,7 +39,8 @@ func ValidatePath(path string) error {
|
||||
return ErrPathEndsWithDelimiter
|
||||
}
|
||||
|
||||
if filepath.Clean(path) != path {
|
||||
// apply `ToSlash` to replace OS-specific separators introduced by the Clean() function
|
||||
if filepath.ToSlash(filepath.Clean(path)) != path {
|
||||
return ErrNonCanonicalPath
|
||||
}
|
||||
|
||||
@@ -156,6 +157,16 @@ type ListOptions struct {
|
||||
Filter PathFilter
|
||||
}
|
||||
|
||||
type DeleteFolderOptions struct {
|
||||
// Force if set to true, the `deleteFolder` operation will delete the selected folder together with all the nested files & folders
|
||||
Force bool
|
||||
|
||||
// AccessFilter must match all the nested files & folders in order for the `deleteFolder` operation to succeed
|
||||
// The access check is not performed if `AccessFilter` is nil
|
||||
AccessFilter PathFilter
|
||||
}
|
||||
|
||||
//go:generate mockery --name FileStorage --structname MockFileStorage --inpackage --filename file_storage_mock.go
|
||||
type FileStorage interface {
|
||||
Get(ctx context.Context, path string) (*File, error)
|
||||
Delete(ctx context.Context, path string) error
|
||||
@@ -165,7 +176,7 @@ type FileStorage interface {
|
||||
List(ctx context.Context, folderPath string, paging *Paging, options *ListOptions) (*ListResponse, error)
|
||||
|
||||
CreateFolder(ctx context.Context, path string) error
|
||||
DeleteFolder(ctx context.Context, path string) error
|
||||
DeleteFolder(ctx context.Context, path string, options *DeleteFolderOptions) error
|
||||
|
||||
close() error
|
||||
}
|
||||
|
||||
@@ -100,6 +100,9 @@ func TestFilestorageApi_ValidatePath(t *testing.T) {
|
||||
{
|
||||
path: "/myFile/file.jpg",
|
||||
},
|
||||
{
|
||||
path: "/file.jpg",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if tt.expectedError == nil {
|
||||
|
||||
@@ -9,10 +9,9 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"gocloud.dev/blob"
|
||||
"gocloud.dev/gcerrors"
|
||||
|
||||
_ "gocloud.dev/blob/fileblob"
|
||||
_ "gocloud.dev/blob/memblob"
|
||||
"gocloud.dev/gcerrors"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -215,20 +214,62 @@ func (c cdkBlobStorage) CreateFolder(ctx context.Context, path string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c cdkBlobStorage) DeleteFolder(ctx context.Context, folderPath string) error {
|
||||
directoryMarkerPath := fmt.Sprintf("%s%s%s", folderPath, Delimiter, directoryMarker)
|
||||
exists, err := c.bucket.Exists(ctx, strings.ToLower(directoryMarkerPath))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
func (c cdkBlobStorage) DeleteFolder(ctx context.Context, folderPath string, options *DeleteFolderOptions) error {
|
||||
folderPrefix := strings.ToLower(c.convertFolderPathToPrefix(folderPath))
|
||||
directoryMarkerPath := folderPrefix + directoryMarker
|
||||
if !options.Force {
|
||||
return c.bucket.Delete(ctx, directoryMarkerPath)
|
||||
}
|
||||
|
||||
if !exists {
|
||||
return nil
|
||||
iterators := []*blob.ListIterator{c.bucket.List(&blob.ListOptions{
|
||||
Prefix: folderPrefix,
|
||||
Delimiter: Delimiter,
|
||||
})}
|
||||
|
||||
var pathsToDelete []string
|
||||
|
||||
for len(iterators) > 0 {
|
||||
obj, err := iterators[0].Next(ctx)
|
||||
if errors.Is(err, io.EOF) {
|
||||
iterators = iterators[1:]
|
||||
continue
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
c.log.Error("force folder delete: failed to retrieve next object", "err", err)
|
||||
return err
|
||||
}
|
||||
|
||||
path := obj.Key
|
||||
lowerPath := strings.ToLower(path)
|
||||
if obj.IsDir {
|
||||
iterators = append([]*blob.ListIterator{c.bucket.List(&blob.ListOptions{
|
||||
Prefix: lowerPath,
|
||||
Delimiter: Delimiter,
|
||||
})}, iterators...)
|
||||
continue
|
||||
}
|
||||
|
||||
pathsToDelete = append(pathsToDelete, lowerPath)
|
||||
}
|
||||
|
||||
err = c.bucket.Delete(ctx, strings.ToLower(directoryMarkerPath))
|
||||
return err
|
||||
for _, path := range pathsToDelete {
|
||||
if !options.AccessFilter.IsAllowed(path) {
|
||||
c.log.Error("force folder delete: unauthorized access", "path", path)
|
||||
return fmt.Errorf("force folder delete error, unauthorized access to %s", path)
|
||||
}
|
||||
}
|
||||
|
||||
var lastErr error
|
||||
for _, path := range pathsToDelete {
|
||||
if err := c.bucket.Delete(ctx, path); err != nil {
|
||||
c.log.Error("force folder delete: failed while deleting a file", "err", err, "path", path)
|
||||
lastErr = err
|
||||
// keep going and delete remaining files
|
||||
}
|
||||
}
|
||||
|
||||
return lastErr
|
||||
}
|
||||
|
||||
//nolint: gocyclo
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"reflect"
|
||||
|
||||
// can ignore because we don't need a cryptographically secure hash function
|
||||
// sha1 low chance of collisions and better performance than sha256
|
||||
@@ -135,30 +136,23 @@ func (s dbFileStorage) Delete(ctx context.Context, filePath string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||
table := &file{}
|
||||
exists, innerErr := sess.Table("file").Where("path_hash = ?", pathHash).Get(table)
|
||||
if innerErr != nil {
|
||||
return innerErr
|
||||
err = s.db.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||
deletedFilesCount, err := sess.Table("file").Where("path_hash = ?", pathHash).Delete(&file{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !exists {
|
||||
return nil
|
||||
deletedMetaCount, err := sess.Table("file_meta").Where("path_hash = ?", pathHash).Delete(&fileMeta{})
|
||||
if err != nil {
|
||||
if rollErr := sess.Rollback(); rollErr != nil {
|
||||
return fmt.Errorf("failed to roll back transaction due to error: %s: %w", rollErr, err)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
number, innerErr := sess.Table("file").Where("path_hash = ?", pathHash).Delete(table)
|
||||
if innerErr != nil {
|
||||
return innerErr
|
||||
}
|
||||
s.log.Info("Deleted file", "path", filePath, "affectedRecords", number)
|
||||
|
||||
metaTable := &fileMeta{}
|
||||
number, innerErr = sess.Table("file_meta").Where("path_hash = ?", pathHash).Delete(metaTable)
|
||||
if innerErr != nil {
|
||||
return innerErr
|
||||
}
|
||||
s.log.Info("Deleted metadata", "path", filePath, "affectedRecords", number)
|
||||
return innerErr
|
||||
s.log.Info("Deleted file", "path", filePath, "deletedMetaCount", deletedMetaCount, "deletedFilesCount", deletedFilesCount)
|
||||
return err
|
||||
})
|
||||
|
||||
return err
|
||||
@@ -490,24 +484,87 @@ func (s dbFileStorage) CreateFolder(ctx context.Context, path string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (s dbFileStorage) DeleteFolder(ctx context.Context, folderPath string) error {
|
||||
err := s.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||
existing := &file{}
|
||||
internalFolderPathHash, err := createPathHash(folderPath + Delimiter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
exists, err := sess.Table("file").Where("path_hash = ?", internalFolderPathHash).Get(existing)
|
||||
func (s dbFileStorage) DeleteFolder(ctx context.Context, folderPath string, options *DeleteFolderOptions) error {
|
||||
lowerFolderPath := strings.ToLower(folderPath)
|
||||
if lowerFolderPath == "" || lowerFolderPath == Delimiter {
|
||||
lowerFolderPath = Delimiter
|
||||
} else if !strings.HasSuffix(lowerFolderPath, Delimiter) {
|
||||
lowerFolderPath = lowerFolderPath + Delimiter
|
||||
}
|
||||
|
||||
if !options.Force {
|
||||
return s.Delete(ctx, lowerFolderPath)
|
||||
}
|
||||
|
||||
err := s.db.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||
var rawHashes []interface{}
|
||||
|
||||
// xorm does not support `.Delete()` with `.Join()`, so we first have to retrieve all path_hashes and then use them to filter `file_meta` table
|
||||
err := sess.Table("file").
|
||||
Cols("path_hash").
|
||||
Where("LOWER(path) LIKE ?", lowerFolderPath+"%").
|
||||
Find(&rawHashes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !exists {
|
||||
if len(rawHashes) == 0 {
|
||||
s.log.Info("Force deleted folder", "path", lowerFolderPath, "deletedFilesCount", 0, "deletedMetaCount", 0)
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err = sess.Table("file").Where("path_hash = ?", internalFolderPathHash).Delete(existing)
|
||||
return err
|
||||
accessFilter := options.AccessFilter.asSQLFilter()
|
||||
accessibleFilesCount, err := sess.Table("file").
|
||||
Cols("path_hash").
|
||||
Where("LOWER(path) LIKE ?", lowerFolderPath+"%").
|
||||
Where(accessFilter.Where, accessFilter.Args...).
|
||||
Count(&file{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if int64(len(rawHashes)) != accessibleFilesCount {
|
||||
s.log.Error("force folder delete: unauthorized access", "path", lowerFolderPath, "expectedAccessibleFilesCount", int64(len(rawHashes)), "actualAccessibleFilesCount", accessibleFilesCount)
|
||||
return fmt.Errorf("force folder delete: unauthorized access for path %s", lowerFolderPath)
|
||||
}
|
||||
|
||||
var hashes []interface{}
|
||||
for _, hash := range rawHashes {
|
||||
if hashString, ok := hash.(string); ok {
|
||||
hashes = append(hashes, hashString)
|
||||
|
||||
// MySQL returns the `path_hash` field as []uint8
|
||||
} else if hashUint, ok := hash.([]uint8); ok {
|
||||
hashes = append(hashes, string(hashUint))
|
||||
} else {
|
||||
return fmt.Errorf("invalid hash type: %s", reflect.TypeOf(hash))
|
||||
}
|
||||
}
|
||||
|
||||
deletedFilesCount, err := sess.
|
||||
Table("file").
|
||||
In("path_hash", hashes...).
|
||||
Delete(&file{})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deletedMetaCount, err := sess.
|
||||
Table("file_meta").
|
||||
In("path_hash", hashes...).
|
||||
Delete(&fileMeta{})
|
||||
|
||||
if err != nil {
|
||||
if rollErr := sess.Rollback(); rollErr != nil {
|
||||
return fmt.Errorf("failed to roll back transaction due to error: %s: %w", rollErr, err)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
s.log.Info("Force deleted folder", "path", folderPath, "deletedFilesCount", deletedFilesCount, "deletedMetaCount", deletedMetaCount)
|
||||
return nil
|
||||
})
|
||||
|
||||
return err
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
// Code generated by mockery v2.10.6. DO NOT EDIT.
|
||||
|
||||
package filestorage
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// MockFileStorage is an autogenerated mock type for the FileStorage type
|
||||
type MockFileStorage struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// CreateFolder provides a mock function with given fields: ctx, path
|
||||
func (_m *MockFileStorage) CreateFolder(ctx context.Context, path string) error {
|
||||
ret := _m.Called(ctx, path)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) error); ok {
|
||||
r0 = rf(ctx, path)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Delete provides a mock function with given fields: ctx, path
|
||||
func (_m *MockFileStorage) Delete(ctx context.Context, path string) error {
|
||||
ret := _m.Called(ctx, path)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) error); ok {
|
||||
r0 = rf(ctx, path)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// DeleteFolder provides a mock function with given fields: ctx, path, options
|
||||
func (_m *MockFileStorage) DeleteFolder(ctx context.Context, path string, options *DeleteFolderOptions) error {
|
||||
ret := _m.Called(ctx, path, options)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, *DeleteFolderOptions) error); ok {
|
||||
r0 = rf(ctx, path, options)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Get provides a mock function with given fields: ctx, path
|
||||
func (_m *MockFileStorage) Get(ctx context.Context, path string) (*File, error) {
|
||||
ret := _m.Called(ctx, path)
|
||||
|
||||
var r0 *File
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) *File); ok {
|
||||
r0 = rf(ctx, path)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*File)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
|
||||
r1 = rf(ctx, path)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// List provides a mock function with given fields: ctx, folderPath, paging, options
|
||||
func (_m *MockFileStorage) List(ctx context.Context, folderPath string, paging *Paging, options *ListOptions) (*ListResponse, error) {
|
||||
ret := _m.Called(ctx, folderPath, paging, options)
|
||||
|
||||
var r0 *ListResponse
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, *Paging, *ListOptions) *ListResponse); ok {
|
||||
r0 = rf(ctx, folderPath, paging, options)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*ListResponse)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string, *Paging, *ListOptions) error); ok {
|
||||
r1 = rf(ctx, folderPath, paging, options)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Upsert provides a mock function with given fields: ctx, command
|
||||
func (_m *MockFileStorage) Upsert(ctx context.Context, command *UpsertFileCommand) error {
|
||||
ret := _m.Called(ctx, command)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *UpsertFileCommand) error); ok {
|
||||
r0 = rf(ctx, command)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// close provides a mock function with given fields:
|
||||
func (_m *MockFileStorage) close() error {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func() error); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
@@ -1183,6 +1183,123 @@ func TestIntegrationFsStorage(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should be able to delete folders with files if using force",
|
||||
steps: []interface{}{
|
||||
cmdCreateFolder{
|
||||
path: "/folder/dashboards/myNewFolder",
|
||||
},
|
||||
cmdUpsert{
|
||||
cmd: UpsertFileCommand{
|
||||
Path: "/folder/dashboards/myNewFolder/file.jpg",
|
||||
Contents: emptyContents,
|
||||
},
|
||||
},
|
||||
cmdDeleteFolder{
|
||||
path: "/folder/dashboards/myNewFolder",
|
||||
options: &DeleteFolderOptions{
|
||||
Force: true,
|
||||
},
|
||||
},
|
||||
queryListFolders{
|
||||
input: queryListFoldersInput{path: "/", options: &ListOptions{Recursive: true}},
|
||||
checks: [][]interface{}{
|
||||
checks(fPath("/folder")),
|
||||
checks(fPath("/folder/dashboards")),
|
||||
},
|
||||
},
|
||||
queryGet{
|
||||
input: queryGetInput{
|
||||
path: "/folder/dashboards/myNewFolder/file.jpg",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should be able to delete root folder with force",
|
||||
steps: []interface{}{
|
||||
cmdCreateFolder{
|
||||
path: "/folder/dashboards/myNewFolder",
|
||||
},
|
||||
cmdUpsert{
|
||||
cmd: UpsertFileCommand{
|
||||
Path: "/folder/dashboards/myNewFolder/file.jpg",
|
||||
Contents: emptyContents,
|
||||
},
|
||||
},
|
||||
cmdDeleteFolder{
|
||||
path: "/",
|
||||
options: &DeleteFolderOptions{
|
||||
Force: true,
|
||||
},
|
||||
},
|
||||
queryListFolders{
|
||||
input: queryListFoldersInput{path: "/", options: &ListOptions{Recursive: true}},
|
||||
checks: [][]interface{}{},
|
||||
},
|
||||
queryGet{
|
||||
input: queryGetInput{
|
||||
path: "/folder/dashboards/myNewFolder/file.jpg",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should not be able to delete a folder unless have access to all nested files",
|
||||
steps: []interface{}{
|
||||
cmdCreateFolder{
|
||||
path: "/folder/dashboards/myNewFolder",
|
||||
},
|
||||
cmdUpsert{
|
||||
cmd: UpsertFileCommand{
|
||||
Path: "/folder/dashboards/myNewFolder/file.jpg",
|
||||
Contents: emptyContents,
|
||||
},
|
||||
},
|
||||
cmdUpsert{
|
||||
cmd: UpsertFileCommand{
|
||||
Path: "/folder/dashboards/abc/file.jpg",
|
||||
Contents: emptyContents,
|
||||
},
|
||||
},
|
||||
cmdDeleteFolder{
|
||||
path: "/",
|
||||
options: &DeleteFolderOptions{
|
||||
Force: true,
|
||||
AccessFilter: NewPathFilter([]string{"/"}, nil, nil, []string{"/folder/dashboards/abc/file.jpg"}),
|
||||
},
|
||||
error: &cmdErrorOutput{
|
||||
message: "force folder delete: unauthorized access for path %s",
|
||||
args: []interface{}{"/"},
|
||||
},
|
||||
},
|
||||
queryListFolders{
|
||||
input: queryListFoldersInput{path: "/", options: &ListOptions{Recursive: true}},
|
||||
checks: [][]interface{}{
|
||||
checks(fPath("/folder")),
|
||||
checks(fPath("/folder/dashboards")),
|
||||
checks(fPath("/folder/dashboards/abc")),
|
||||
checks(fPath("/folder/dashboards/myNewFolder")),
|
||||
},
|
||||
},
|
||||
queryGet{
|
||||
input: queryGetInput{
|
||||
path: "/folder/dashboards/myNewFolder/file.jpg",
|
||||
},
|
||||
checks: checks(
|
||||
fName("file.jpg"),
|
||||
),
|
||||
},
|
||||
queryGet{
|
||||
input: queryGetInput{
|
||||
path: "/folder/dashboards/myNewFolder/file.jpg",
|
||||
},
|
||||
checks: checks(
|
||||
fName("file.jpg"),
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,8 +32,9 @@ type cmdCreateFolder struct {
|
||||
}
|
||||
|
||||
type cmdDeleteFolder struct {
|
||||
path string
|
||||
error *cmdErrorOutput
|
||||
path string
|
||||
error *cmdErrorOutput
|
||||
options *DeleteFolderOptions
|
||||
}
|
||||
|
||||
type queryGetInput struct {
|
||||
@@ -175,7 +176,7 @@ func handleCommand(t *testing.T, ctx context.Context, cmd interface{}, cmdName s
|
||||
}
|
||||
expectedErr = c.error
|
||||
case cmdDeleteFolder:
|
||||
err = fs.DeleteFolder(ctx, c.path)
|
||||
err = fs.DeleteFolder(ctx, c.path, c.options)
|
||||
if c.error == nil {
|
||||
require.NoError(t, err, "%s: should be able to delete %s", cmdName, c.path)
|
||||
}
|
||||
|
||||
@@ -256,26 +256,58 @@ func (b wrapper) CreateFolder(ctx context.Context, path string) error {
|
||||
return b.wrapped.CreateFolder(ctx, rootedPath)
|
||||
}
|
||||
|
||||
func (b wrapper) DeleteFolder(ctx context.Context, path string) error {
|
||||
func (b wrapper) deleteFolderOptionsWithDefaults(options *DeleteFolderOptions) *DeleteFolderOptions {
|
||||
if options == nil {
|
||||
return &DeleteFolderOptions{
|
||||
Force: false,
|
||||
AccessFilter: b.filter,
|
||||
}
|
||||
}
|
||||
|
||||
if options.AccessFilter == nil {
|
||||
return &DeleteFolderOptions{
|
||||
Force: options.Force,
|
||||
AccessFilter: b.filter,
|
||||
}
|
||||
}
|
||||
|
||||
var filter PathFilter
|
||||
if options.AccessFilter != nil {
|
||||
filter = newAndPathFilter(b.filter, wrapPathFilter(options.AccessFilter, b.rootFolder))
|
||||
} else {
|
||||
filter = b.filter
|
||||
}
|
||||
|
||||
return &DeleteFolderOptions{
|
||||
Force: options.Force,
|
||||
AccessFilter: filter,
|
||||
}
|
||||
}
|
||||
|
||||
func (b wrapper) DeleteFolder(ctx context.Context, path string, options *DeleteFolderOptions) error {
|
||||
if err := b.validatePath(path); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rootedPath := b.addRoot(path)
|
||||
if !b.filter.IsAllowed(rootedPath) {
|
||||
return nil
|
||||
|
||||
optionsWithDefaults := b.deleteFolderOptionsWithDefaults(options)
|
||||
if !optionsWithDefaults.AccessFilter.IsAllowed(rootedPath) {
|
||||
return fmt.Errorf("delete folder unauthorized - no access to %s", rootedPath)
|
||||
}
|
||||
|
||||
isEmpty, err := b.isFolderEmpty(ctx, path)
|
||||
if err != nil {
|
||||
return err
|
||||
if !optionsWithDefaults.Force {
|
||||
isEmpty, err := b.isFolderEmpty(ctx, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !isEmpty {
|
||||
return fmt.Errorf("folder %s is not empty - cant remove it", path)
|
||||
}
|
||||
}
|
||||
|
||||
if !isEmpty {
|
||||
return fmt.Errorf("folder %s is not empty - cant remove it", path)
|
||||
}
|
||||
|
||||
return b.wrapped.DeleteFolder(ctx, rootedPath)
|
||||
return b.wrapped.DeleteFolder(ctx, rootedPath, optionsWithDefaults)
|
||||
}
|
||||
|
||||
func (b wrapper) List(ctx context.Context, folderPath string, paging *Paging, options *ListOptions) (*ListResponse, error) {
|
||||
|
||||
@@ -1,264 +0,0 @@
|
||||
package level
|
||||
|
||||
import (
|
||||
"github.com/go-kit/log"
|
||||
gokitlevel "github.com/go-kit/log/level"
|
||||
)
|
||||
|
||||
// Error returns a logger that includes a Key/ErrorValue pair.
|
||||
func Error(logger log.Logger) log.Logger {
|
||||
return log.WithPrefix(logger, Key(), ErrorValue())
|
||||
}
|
||||
|
||||
// Warn returns a logger that includes a Key/WarnValue pair.
|
||||
func Warn(logger log.Logger) log.Logger {
|
||||
return log.WithPrefix(logger, Key(), WarnValue())
|
||||
}
|
||||
|
||||
// Info returns a logger that includes a Key/InfoValue pair.
|
||||
func Info(logger log.Logger) log.Logger {
|
||||
return log.WithPrefix(logger, Key(), InfoValue())
|
||||
}
|
||||
|
||||
// Debug returns a logger that includes a Key/DebugValue pair.
|
||||
func Debug(logger log.Logger) log.Logger {
|
||||
return log.WithPrefix(logger, Key(), DebugValue())
|
||||
}
|
||||
|
||||
// NewFilter wraps next and implements level filtering. See the commentary on
|
||||
// the Option functions for a detailed description of how to configure levels.
|
||||
// If no options are provided, all leveled log events created with Debug,
|
||||
// Info, Warn or Error helper methods are squelched and non-leveled log
|
||||
// events are passed to next unmodified.
|
||||
func NewFilter(next log.Logger, options ...Option) log.Logger {
|
||||
l := &logger{
|
||||
next: next,
|
||||
}
|
||||
for _, option := range options {
|
||||
option(l)
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
type logger struct {
|
||||
next log.Logger
|
||||
allowed level
|
||||
squelchNoLevel bool
|
||||
errNotAllowed error
|
||||
errNoLevel error
|
||||
}
|
||||
|
||||
func (l *logger) Log(keyvals ...interface{}) error {
|
||||
var hasLevel, levelAllowed bool
|
||||
for i := 1; i < len(keyvals); i += 2 {
|
||||
if v, ok := keyvals[i].(*levelValue); ok {
|
||||
hasLevel = true
|
||||
levelAllowed = l.allowed&v.level != 0
|
||||
break
|
||||
}
|
||||
|
||||
if v, ok := keyvals[i].(gokitlevel.Value); ok {
|
||||
hasLevel = true
|
||||
levelAllowed = l.allowed&levelFromGokitLevel(v) != 0
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasLevel && l.squelchNoLevel {
|
||||
return l.errNoLevel
|
||||
}
|
||||
if hasLevel && !levelAllowed {
|
||||
return l.errNotAllowed
|
||||
}
|
||||
return l.next.Log(keyvals...)
|
||||
}
|
||||
|
||||
// Option sets a parameter for the leveled logger.
|
||||
type Option func(*logger)
|
||||
|
||||
// AllowAll is an alias for AllowDebug.
|
||||
func AllowAll() Option {
|
||||
return AllowDebug()
|
||||
}
|
||||
|
||||
// AllowDebug allows error, warn, info and debug level log events to pass.
|
||||
func AllowDebug() Option {
|
||||
return allowed(levelError | levelWarn | levelInfo | levelDebug)
|
||||
}
|
||||
|
||||
// AllowInfo allows error, warn and info level log events to pass.
|
||||
func AllowInfo() Option {
|
||||
return allowed(levelError | levelWarn | levelInfo)
|
||||
}
|
||||
|
||||
// AllowWarn allows error and warn level log events to pass.
|
||||
func AllowWarn() Option {
|
||||
return allowed(levelError | levelWarn)
|
||||
}
|
||||
|
||||
// AllowError allows only error level log events to pass.
|
||||
func AllowError() Option {
|
||||
return allowed(levelError)
|
||||
}
|
||||
|
||||
// AllowNone allows no leveled log events to pass.
|
||||
func AllowNone() Option {
|
||||
return allowed(0)
|
||||
}
|
||||
|
||||
func allowed(allowed level) Option {
|
||||
return func(l *logger) { l.allowed = allowed }
|
||||
}
|
||||
|
||||
// ErrNotAllowed sets the error to return from Log when it squelches a log
|
||||
// event disallowed by the configured Allow[Level] option. By default,
|
||||
// ErrNotAllowed is nil; in this case the log event is squelched with no
|
||||
// error.
|
||||
func ErrNotAllowed(err error) Option {
|
||||
return func(l *logger) { l.errNotAllowed = err }
|
||||
}
|
||||
|
||||
// SquelchNoLevel instructs Log to squelch log events with no level, so that
|
||||
// they don't proceed through to the wrapped logger. If SquelchNoLevel is set
|
||||
// to true and a log event is squelched in this way, the error value
|
||||
// configured with ErrNoLevel is returned to the caller.
|
||||
func SquelchNoLevel(squelch bool) Option {
|
||||
return func(l *logger) { l.squelchNoLevel = squelch }
|
||||
}
|
||||
|
||||
// ErrNoLevel sets the error to return from Log when it squelches a log event
|
||||
// with no level. By default, ErrNoLevel is nil; in this case the log event is
|
||||
// squelched with no error.
|
||||
func ErrNoLevel(err error) Option {
|
||||
return func(l *logger) { l.errNoLevel = err }
|
||||
}
|
||||
|
||||
// NewInjector wraps next and returns a logger that adds a Key/level pair to
|
||||
// the beginning of log events that don't already contain a level. In effect,
|
||||
// this gives a default level to logs without a level.
|
||||
func NewInjector(next log.Logger, level Value) log.Logger {
|
||||
return &injector{
|
||||
next: next,
|
||||
level: level,
|
||||
}
|
||||
}
|
||||
|
||||
type injector struct {
|
||||
next log.Logger
|
||||
level interface{}
|
||||
}
|
||||
|
||||
func (l *injector) Log(keyvals ...interface{}) error {
|
||||
for i := 1; i < len(keyvals); i += 2 {
|
||||
if _, ok := keyvals[i].(*levelValue); ok {
|
||||
return l.next.Log(keyvals...)
|
||||
}
|
||||
}
|
||||
kvs := make([]interface{}, len(keyvals)+2)
|
||||
kvs[0], kvs[1] = key, l.level
|
||||
copy(kvs[2:], keyvals)
|
||||
return l.next.Log(kvs...)
|
||||
}
|
||||
|
||||
// Value is the interface that each of the canonical level values implement.
|
||||
// It contains unexported methods that prevent types from other packages from
|
||||
// implementing it and guaranteeing that NewFilter can distinguish the levels
|
||||
// defined in this package from all other values.
|
||||
type Value interface {
|
||||
String() string
|
||||
levelVal()
|
||||
}
|
||||
|
||||
// Key returns the unique key added to log events by the loggers in this
|
||||
// package.
|
||||
func Key() interface{} { return key }
|
||||
|
||||
// ErrorValue returns the unique value added to log events by Error.
|
||||
func ErrorValue() Value { return errorValue }
|
||||
|
||||
// WarnValue returns the unique value added to log events by Warn.
|
||||
func WarnValue() Value { return warnValue }
|
||||
|
||||
// InfoValue returns the unique value added to log events by Info.
|
||||
func InfoValue() Value { return infoValue }
|
||||
|
||||
// DebugValue returns the unique value added to log events by Debug.
|
||||
func DebugValue() Value { return debugValue }
|
||||
|
||||
var (
|
||||
// key is of type interface{} so that it allocates once during package
|
||||
// initialization and avoids allocating every time the value is added to a
|
||||
// []interface{} later.
|
||||
key interface{} = "lvl"
|
||||
|
||||
errorValue = &levelValue{level: levelError, name: "eror"}
|
||||
warnValue = &levelValue{level: levelWarn, name: "warn"}
|
||||
infoValue = &levelValue{level: levelInfo, name: "info"}
|
||||
debugValue = &levelValue{level: levelDebug, name: "dbug"}
|
||||
)
|
||||
|
||||
func SetLevelKeyAndValuesToGokitLog() {
|
||||
key = "level"
|
||||
errorValue = &levelValue{level: levelError, name: "error"}
|
||||
warnValue = &levelValue{level: levelWarn, name: "warn"}
|
||||
infoValue = &levelValue{level: levelInfo, name: "info"}
|
||||
debugValue = &levelValue{level: levelDebug, name: "debug"}
|
||||
}
|
||||
|
||||
type level byte
|
||||
|
||||
const (
|
||||
levelDebug level = 1 << iota
|
||||
levelInfo
|
||||
levelWarn
|
||||
levelError
|
||||
)
|
||||
|
||||
func IsKey(v interface{}) bool {
|
||||
return v != nil && (v == Key() || v == gokitlevel.Key())
|
||||
}
|
||||
|
||||
func GetValue(v interface{}) Value {
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if val, ok := v.(Value); ok {
|
||||
return val
|
||||
}
|
||||
|
||||
if val, ok := v.(gokitlevel.Value); ok {
|
||||
switch val {
|
||||
case gokitlevel.InfoValue():
|
||||
return InfoValue()
|
||||
case gokitlevel.WarnValue():
|
||||
return WarnValue()
|
||||
case gokitlevel.ErrorValue():
|
||||
return ErrorValue()
|
||||
case gokitlevel.DebugValue():
|
||||
return DebugValue()
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type levelValue struct {
|
||||
name string
|
||||
level
|
||||
}
|
||||
|
||||
func (v *levelValue) String() string { return v.name }
|
||||
func (v *levelValue) levelVal() {}
|
||||
|
||||
func levelFromGokitLevel(l gokitlevel.Value) level {
|
||||
switch l.String() {
|
||||
case gokitlevel.ErrorValue().String():
|
||||
return levelError
|
||||
case gokitlevel.WarnValue().String():
|
||||
return levelWarn
|
||||
case gokitlevel.DebugValue().String():
|
||||
return levelDebug
|
||||
}
|
||||
|
||||
return levelInfo
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
package level_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
gokitlog "github.com/go-kit/log"
|
||||
gokitlevel "github.com/go-kit/log/level"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/log/level"
|
||||
)
|
||||
|
||||
func TestNewFilter(t *testing.T) {
|
||||
newFilteredLoggerScenario(t, "Given all levels is allowed should log all messages", level.AllowAll(), func(t *testing.T, ctx *scenarioContext) {
|
||||
logTestMessages(t, ctx)
|
||||
|
||||
require.Len(t, ctx.loggedArgs, 8)
|
||||
require.Equal(t, "lvl", ctx.loggedArgs[0][2].(string))
|
||||
require.Equal(t, "info", ctx.loggedArgs[0][3].(level.Value).String())
|
||||
require.Equal(t, "lvl", ctx.loggedArgs[1][2].(string))
|
||||
require.Equal(t, "warn", ctx.loggedArgs[1][3].(level.Value).String())
|
||||
require.Equal(t, "lvl", ctx.loggedArgs[2][2].(string))
|
||||
require.Equal(t, "eror", ctx.loggedArgs[2][3].(level.Value).String())
|
||||
require.Equal(t, "lvl", ctx.loggedArgs[3][2].(string))
|
||||
require.Equal(t, "dbug", ctx.loggedArgs[3][3].(level.Value).String())
|
||||
|
||||
require.Equal(t, "level", ctx.loggedArgs[4][2].(string))
|
||||
require.Equal(t, "info", ctx.loggedArgs[4][3].(gokitlevel.Value).String())
|
||||
require.Equal(t, "level", ctx.loggedArgs[5][2].(string))
|
||||
require.Equal(t, "warn", ctx.loggedArgs[5][3].(gokitlevel.Value).String())
|
||||
require.Equal(t, "level", ctx.loggedArgs[6][2].(string))
|
||||
require.Equal(t, "error", ctx.loggedArgs[6][3].(gokitlevel.Value).String())
|
||||
require.Equal(t, "level", ctx.loggedArgs[7][2].(string))
|
||||
require.Equal(t, "debug", ctx.loggedArgs[7][3].(gokitlevel.Value).String())
|
||||
})
|
||||
|
||||
newFilteredLoggerScenario(t, "Given error, warnings, info, debug is allowed should log all messages", level.AllowDebug(), func(t *testing.T, ctx *scenarioContext) {
|
||||
logTestMessages(t, ctx)
|
||||
|
||||
require.Len(t, ctx.loggedArgs, 8)
|
||||
require.Equal(t, "lvl", ctx.loggedArgs[0][2].(string))
|
||||
require.Equal(t, "info", ctx.loggedArgs[0][3].(level.Value).String())
|
||||
require.Equal(t, "lvl", ctx.loggedArgs[1][2].(string))
|
||||
require.Equal(t, "warn", ctx.loggedArgs[1][3].(level.Value).String())
|
||||
require.Equal(t, "lvl", ctx.loggedArgs[2][2].(string))
|
||||
require.Equal(t, "eror", ctx.loggedArgs[2][3].(level.Value).String())
|
||||
require.Equal(t, "lvl", ctx.loggedArgs[3][2].(string))
|
||||
require.Equal(t, "dbug", ctx.loggedArgs[3][3].(level.Value).String())
|
||||
|
||||
require.Equal(t, "level", ctx.loggedArgs[4][2].(string))
|
||||
require.Equal(t, "info", ctx.loggedArgs[4][3].(gokitlevel.Value).String())
|
||||
require.Equal(t, "level", ctx.loggedArgs[5][2].(string))
|
||||
require.Equal(t, "warn", ctx.loggedArgs[5][3].(gokitlevel.Value).String())
|
||||
require.Equal(t, "level", ctx.loggedArgs[6][2].(string))
|
||||
require.Equal(t, "error", ctx.loggedArgs[6][3].(gokitlevel.Value).String())
|
||||
require.Equal(t, "level", ctx.loggedArgs[7][2].(string))
|
||||
require.Equal(t, "debug", ctx.loggedArgs[7][3].(gokitlevel.Value).String())
|
||||
})
|
||||
|
||||
newFilteredLoggerScenario(t, "Given error, warnings is allowed should log error and warning messages", level.AllowWarn(), func(t *testing.T, ctx *scenarioContext) {
|
||||
logTestMessages(t, ctx)
|
||||
|
||||
require.Len(t, ctx.loggedArgs, 4)
|
||||
require.Equal(t, "lvl", ctx.loggedArgs[0][2].(string))
|
||||
require.Equal(t, "warn", ctx.loggedArgs[0][3].(level.Value).String())
|
||||
require.Equal(t, "lvl", ctx.loggedArgs[1][2].(string))
|
||||
require.Equal(t, "eror", ctx.loggedArgs[1][3].(level.Value).String())
|
||||
|
||||
require.Equal(t, "level", ctx.loggedArgs[2][2].(string))
|
||||
require.Equal(t, "warn", ctx.loggedArgs[2][3].(gokitlevel.Value).String())
|
||||
require.Equal(t, "level", ctx.loggedArgs[3][2].(string))
|
||||
require.Equal(t, "error", ctx.loggedArgs[3][3].(gokitlevel.Value).String())
|
||||
})
|
||||
|
||||
newFilteredLoggerScenario(t, "Given error allowed should log error messages", level.AllowError(), func(t *testing.T, ctx *scenarioContext) {
|
||||
logTestMessages(t, ctx)
|
||||
|
||||
require.Len(t, ctx.loggedArgs, 2)
|
||||
require.Equal(t, "lvl", ctx.loggedArgs[0][2].(string))
|
||||
require.Equal(t, "eror", ctx.loggedArgs[0][3].(level.Value).String())
|
||||
|
||||
require.Equal(t, "level", ctx.loggedArgs[1][2].(string))
|
||||
require.Equal(t, "error", ctx.loggedArgs[1][3].(gokitlevel.Value).String())
|
||||
})
|
||||
|
||||
newFilteredLoggerScenario(t, "Given error, warnings, info is allowed should log error, warning and info messages", level.AllowInfo(), func(t *testing.T, ctx *scenarioContext) {
|
||||
logTestMessages(t, ctx)
|
||||
|
||||
require.Len(t, ctx.loggedArgs, 6)
|
||||
require.Equal(t, "lvl", ctx.loggedArgs[0][2].(string))
|
||||
require.Equal(t, "info", ctx.loggedArgs[0][3].(level.Value).String())
|
||||
require.Equal(t, "lvl", ctx.loggedArgs[1][2].(string))
|
||||
require.Equal(t, "warn", ctx.loggedArgs[1][3].(level.Value).String())
|
||||
require.Equal(t, "lvl", ctx.loggedArgs[2][2].(string))
|
||||
require.Equal(t, "eror", ctx.loggedArgs[2][3].(level.Value).String())
|
||||
|
||||
require.Equal(t, "level", ctx.loggedArgs[3][2].(string))
|
||||
require.Equal(t, "info", ctx.loggedArgs[3][3].(gokitlevel.Value).String())
|
||||
require.Equal(t, "level", ctx.loggedArgs[4][2].(string))
|
||||
require.Equal(t, "warn", ctx.loggedArgs[4][3].(gokitlevel.Value).String())
|
||||
require.Equal(t, "level", ctx.loggedArgs[5][2].(string))
|
||||
require.Equal(t, "error", ctx.loggedArgs[5][3].(gokitlevel.Value).String())
|
||||
})
|
||||
|
||||
newFilteredLoggerScenario(t, "Given no levels is allowed should not log any messages", level.AllowNone(), func(t *testing.T, ctx *scenarioContext) {
|
||||
logTestMessages(t, ctx)
|
||||
|
||||
require.Len(t, ctx.loggedArgs, 0)
|
||||
})
|
||||
}
|
||||
|
||||
func logTestMessages(t *testing.T, ctx *scenarioContext) {
|
||||
t.Helper()
|
||||
|
||||
ctx.logger.Info("info msg")
|
||||
ctx.logger.Warn("warn msg")
|
||||
ctx.logger.Error("error msg")
|
||||
ctx.logger.Debug("debug msg")
|
||||
err := gokitlevel.Info(ctx.logger).Log("msg", "gokit info msg")
|
||||
require.NoError(t, err)
|
||||
err = gokitlevel.Warn(ctx.logger).Log("msg", "gokit warn msg")
|
||||
require.NoError(t, err)
|
||||
err = gokitlevel.Error(ctx.logger).Log("msg", "gokit error msg")
|
||||
require.NoError(t, err)
|
||||
err = gokitlevel.Debug(ctx.logger).Log("msg", "gokit debug msg")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
type scenarioContext struct {
|
||||
loggedArgs [][]interface{}
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
func newFilteredLoggerScenario(t *testing.T, desc string, option level.Option, fn func(t *testing.T, ctx *scenarioContext)) {
|
||||
t.Helper()
|
||||
|
||||
ctx := &scenarioContext{
|
||||
loggedArgs: [][]interface{}{},
|
||||
}
|
||||
|
||||
l := gokitlog.LoggerFunc(func(i ...interface{}) error {
|
||||
ctx.loggedArgs = append(ctx.loggedArgs, i)
|
||||
return nil
|
||||
})
|
||||
filteredLogger := level.NewFilter(l, option)
|
||||
testLogger := log.New("test")
|
||||
testLogger.Swap(filteredLogger)
|
||||
|
||||
ctx.logger = testLogger
|
||||
|
||||
t.Run(desc, func(t *testing.T) {
|
||||
fn(t, ctx)
|
||||
})
|
||||
}
|
||||
+5
-62
@@ -11,17 +11,16 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
gokitlog "github.com/go-kit/log"
|
||||
"github.com/go-kit/log/level"
|
||||
"github.com/go-stack/stack"
|
||||
"github.com/mattn/go-isatty"
|
||||
"gopkg.in/ini.v1"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log/level"
|
||||
"github.com/grafana/grafana/pkg/infra/log/term"
|
||||
"github.com/grafana/grafana/pkg/infra/log/text"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
@@ -32,7 +31,7 @@ var (
|
||||
loggersToReload []ReloadableHandler
|
||||
root *logManager
|
||||
now = time.Now
|
||||
logTimeFormat = "2006-01-02T15:04:05.99-0700"
|
||||
logTimeFormat = time.RFC3339Nano
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -56,10 +55,9 @@ func init() {
|
||||
// logManager manage loggers
|
||||
type logManager struct {
|
||||
*ConcreteLogger
|
||||
loggersByName map[string]*ConcreteLogger
|
||||
logFilters []logWithFilters
|
||||
mutex sync.RWMutex
|
||||
gokitLogActivated bool
|
||||
loggersByName map[string]*ConcreteLogger
|
||||
logFilters []logWithFilters
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
func newManager(logger gokitlog.Logger) *logManager {
|
||||
@@ -73,12 +71,6 @@ func (lm *logManager) initialize(loggers []logWithFilters) {
|
||||
lm.mutex.Lock()
|
||||
defer lm.mutex.Unlock()
|
||||
|
||||
if lm.gokitLogActivated {
|
||||
level.SetLevelKeyAndValuesToGokitLog()
|
||||
term.SetTimeFormatGokitLog()
|
||||
logTimeFormat = time.RFC3339Nano
|
||||
}
|
||||
|
||||
defaultLoggers := make([]gokitlog.Logger, len(loggers))
|
||||
for index, logger := range loggers {
|
||||
defaultLoggers[index] = level.NewFilter(logger.val, logger.maxLevel)
|
||||
@@ -452,58 +444,9 @@ func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) error {
|
||||
handler.maxLevel = leveloption
|
||||
configLoggers = append(configLoggers, handler)
|
||||
}
|
||||
|
||||
var err error
|
||||
isOldLoggerActivated, err := isOldLoggerActivated(cfg)
|
||||
root.gokitLogActivated = !isOldLoggerActivated
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(configLoggers) > 0 {
|
||||
root.initialize(configLoggers)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// This would be removed eventually, no need to make a fancy design.
|
||||
// For the sake of important cycle I just copied the function
|
||||
func isOldLoggerActivated(cfg *ini.File) (bool, error) {
|
||||
section := cfg.Section("feature_toggles")
|
||||
toggles, err := readFeatureTogglesFromInitFile(section)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return toggles["oldlog"], nil
|
||||
}
|
||||
|
||||
func readFeatureTogglesFromInitFile(featureTogglesSection *ini.Section) (map[string]bool, error) {
|
||||
featureToggles := make(map[string]bool, 10)
|
||||
|
||||
// parse the comma separated list in `enable`.
|
||||
featuresTogglesStr := valueAsString(featureTogglesSection, "enable", "")
|
||||
for _, feature := range util.SplitString(featuresTogglesStr) {
|
||||
featureToggles[feature] = true
|
||||
}
|
||||
|
||||
// read all other settings under [feature_toggles]. If a toggle is
|
||||
// present in both the value in `enable` is overridden.
|
||||
for _, v := range featureTogglesSection.Keys() {
|
||||
if v.Name() == "enable" {
|
||||
continue
|
||||
}
|
||||
|
||||
b, err := strconv.ParseBool(v.Value())
|
||||
if err != nil {
|
||||
return featureToggles, err
|
||||
}
|
||||
|
||||
featureToggles[v.Name()] = b
|
||||
}
|
||||
return featureToggles, nil
|
||||
}
|
||||
|
||||
func valueAsString(section *ini.Section, keyName string, defaultValue string) string {
|
||||
return section.Key(keyName).MustString(defaultValue)
|
||||
}
|
||||
|
||||
+12
-12
@@ -8,7 +8,7 @@ import (
|
||||
gokitlog "github.com/go-kit/log"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log/level"
|
||||
"github.com/go-kit/log/level"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
@@ -38,7 +38,7 @@ func TestLogger(t *testing.T) {
|
||||
require.Equal(t, "logger", ctx.loggedArgs[0][0].(string))
|
||||
require.Equal(t, "one", ctx.loggedArgs[0][1].(string))
|
||||
require.Equal(t, "t", ctx.loggedArgs[0][2].(string))
|
||||
require.Equal(t, ctx.mockedTime.Format("2006-01-02T15:04:05.99-0700"), ctx.loggedArgs[0][3].(fmt.Stringer).String())
|
||||
require.Equal(t, ctx.mockedTime.Format(time.RFC3339Nano), ctx.loggedArgs[0][3].(fmt.Stringer).String())
|
||||
require.Equal(t, "msg", ctx.loggedArgs[0][4].(string))
|
||||
require.Equal(t, "hello 1", ctx.loggedArgs[0][5].(string))
|
||||
|
||||
@@ -46,7 +46,7 @@ func TestLogger(t *testing.T) {
|
||||
require.Equal(t, "logger", ctx.loggedArgs[1][0].(string))
|
||||
require.Equal(t, "two", ctx.loggedArgs[1][1].(string))
|
||||
require.Equal(t, "t", ctx.loggedArgs[0][2].(string))
|
||||
require.Equal(t, ctx.mockedTime.Format("2006-01-02T15:04:05.99-0700"), ctx.loggedArgs[0][3].(fmt.Stringer).String())
|
||||
require.Equal(t, ctx.mockedTime.Format(time.RFC3339Nano), ctx.loggedArgs[0][3].(fmt.Stringer).String())
|
||||
require.Equal(t, "msg", ctx.loggedArgs[1][4].(string))
|
||||
require.Equal(t, "hello 2", ctx.loggedArgs[1][5].(string))
|
||||
|
||||
@@ -54,8 +54,8 @@ func TestLogger(t *testing.T) {
|
||||
require.Equal(t, "logger", ctx.loggedArgs[2][0].(string))
|
||||
require.Equal(t, "three", ctx.loggedArgs[2][1].(string))
|
||||
require.Equal(t, "t", ctx.loggedArgs[2][2].(string))
|
||||
require.Equal(t, ctx.mockedTime.Format("2006-01-02T15:04:05.99-0700"), ctx.loggedArgs[2][3].(fmt.Stringer).String())
|
||||
require.Equal(t, "lvl", ctx.loggedArgs[2][4].(string))
|
||||
require.Equal(t, ctx.mockedTime.Format(time.RFC3339Nano), ctx.loggedArgs[2][3].(fmt.Stringer).String())
|
||||
require.Equal(t, level.Key().(string), ctx.loggedArgs[2][4].(string))
|
||||
require.Equal(t, level.ErrorValue(), ctx.loggedArgs[2][5].(level.Value))
|
||||
require.Equal(t, "msg", ctx.loggedArgs[2][6].(string))
|
||||
require.Equal(t, "hello 3", ctx.loggedArgs[2][7].(string))
|
||||
@@ -66,7 +66,7 @@ func TestLogger(t *testing.T) {
|
||||
require.Equal(t, "key", ctx.loggedArgs[3][2].(string))
|
||||
require.Equal(t, "value", ctx.loggedArgs[3][3].(string))
|
||||
require.Equal(t, "t", ctx.loggedArgs[3][4].(string))
|
||||
require.Equal(t, ctx.mockedTime.Format("2006-01-02T15:04:05.99-0700"), ctx.loggedArgs[3][5].(fmt.Stringer).String())
|
||||
require.Equal(t, ctx.mockedTime.Format(time.RFC3339Nano), ctx.loggedArgs[3][5].(fmt.Stringer).String())
|
||||
require.Equal(t, "msg", ctx.loggedArgs[3][6].(string))
|
||||
require.Equal(t, "hello 4", ctx.loggedArgs[3][7].(string))
|
||||
|
||||
@@ -74,8 +74,8 @@ func TestLogger(t *testing.T) {
|
||||
require.Equal(t, "logger", ctx.loggedArgs[4][0].(string))
|
||||
require.Equal(t, "three", ctx.loggedArgs[4][1].(string))
|
||||
require.Equal(t, "t", ctx.loggedArgs[4][2].(string))
|
||||
require.Equal(t, ctx.mockedTime.Format("2006-01-02T15:04:05.99-0700"), ctx.loggedArgs[4][3].(fmt.Stringer).String())
|
||||
require.Equal(t, "lvl", ctx.loggedArgs[4][4].(string))
|
||||
require.Equal(t, ctx.mockedTime.Format(time.RFC3339Nano), ctx.loggedArgs[4][3].(fmt.Stringer).String())
|
||||
require.Equal(t, level.Key().(string), ctx.loggedArgs[4][4].(string))
|
||||
require.Equal(t, level.ErrorValue(), ctx.loggedArgs[4][5].(level.Value))
|
||||
require.Equal(t, "msg", ctx.loggedArgs[4][6].(string))
|
||||
require.Equal(t, "hello 3 again", ctx.loggedArgs[4][7].(string))
|
||||
@@ -124,8 +124,8 @@ func TestWithPrefix(t *testing.T) {
|
||||
require.Equal(t, "k1", args[2].(string))
|
||||
require.Equal(t, "v1", args[3].(string))
|
||||
require.Equal(t, "t", args[4].(string))
|
||||
require.Equal(t, ctx.mockedTime.Format("2006-01-02T15:04:05.99-0700"), args[5].(fmt.Stringer).String())
|
||||
require.Equal(t, "lvl", args[6].(string))
|
||||
require.Equal(t, ctx.mockedTime.Format(time.RFC3339Nano), args[5].(fmt.Stringer).String())
|
||||
require.Equal(t, level.Key().(string), args[6].(string))
|
||||
require.Equal(t, level.InfoValue(), args[7].(level.Value))
|
||||
require.Equal(t, "msg", args[8].(string))
|
||||
require.Equal(t, "hello", args[9].(string))
|
||||
@@ -145,8 +145,8 @@ func TestWithSuffix(t *testing.T) {
|
||||
require.Equal(t, "logger", args[0].(string))
|
||||
require.Equal(t, "test", args[1].(string))
|
||||
require.Equal(t, "t", args[2].(string))
|
||||
require.Equal(t, ctx.mockedTime.Format("2006-01-02T15:04:05.99-0700"), args[3].(fmt.Stringer).String())
|
||||
require.Equal(t, "lvl", args[4].(string))
|
||||
require.Equal(t, ctx.mockedTime.Format(time.RFC3339Nano), args[3].(fmt.Stringer).String())
|
||||
require.Equal(t, level.Key().(string), args[4].(string))
|
||||
require.Equal(t, level.InfoValue(), args[5].(level.Value))
|
||||
require.Equal(t, "msg", args[6].(string))
|
||||
require.Equal(t, "hello", args[7].(string))
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/go-kit/log"
|
||||
"github.com/go-kit/log/level"
|
||||
gokitsyslog "github.com/go-kit/log/syslog"
|
||||
"github.com/grafana/grafana/pkg/infra/log/level"
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
@@ -25,8 +25,8 @@ type SysLogHandler struct {
|
||||
|
||||
var selector = func(keyvals ...interface{}) syslog.Priority {
|
||||
for i := 0; i < len(keyvals); i += 2 {
|
||||
if level.IsKey(keyvals[i]) {
|
||||
val := level.GetValue(keyvals[i+1])
|
||||
if keyvals[i] == level.Key() {
|
||||
val := keyvals[i+1]
|
||||
if val != nil {
|
||||
switch val {
|
||||
case level.ErrorValue():
|
||||
@@ -39,11 +39,9 @@ var selector = func(keyvals ...interface{}) syslog.Priority {
|
||||
return syslog.LOG_DEBUG
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return syslog.LOG_INFO
|
||||
}
|
||||
|
||||
|
||||
@@ -11,11 +11,11 @@ import (
|
||||
"time"
|
||||
|
||||
gokitlog "github.com/go-kit/log"
|
||||
"github.com/grafana/grafana/pkg/infra/log/level"
|
||||
"github.com/go-kit/log/level"
|
||||
)
|
||||
|
||||
var (
|
||||
timeFormat = "2006-01-02T15:04:05-0700"
|
||||
timeFormat = time.RFC3339Nano
|
||||
termTimeFormat = "01-02|15:04:05"
|
||||
)
|
||||
|
||||
@@ -25,10 +25,6 @@ const (
|
||||
errorKey = "LOG15_ERROR"
|
||||
)
|
||||
|
||||
func SetTimeFormatGokitLog() {
|
||||
timeFormat = time.RFC3339Nano
|
||||
}
|
||||
|
||||
type terminalLogger struct {
|
||||
w io.Writer
|
||||
}
|
||||
@@ -96,7 +92,6 @@ func getRecord(keyvals ...interface{}) *record {
|
||||
if len(keyvals)%2 == 1 {
|
||||
keyvals = append(keyvals, nil)
|
||||
}
|
||||
|
||||
for i := 0; i < len(keyvals); i += 2 {
|
||||
k, v := keyvals[i], keyvals[i+1]
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/log/level"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/log/level"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"go.etcd.io/etcd/api/v3/version"
|
||||
jaegerpropagator "go.opentelemetry.io/contrib/propagators/jaeger"
|
||||
|
||||
@@ -11,8 +11,8 @@ import (
|
||||
"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/infra/log/level"
|
||||
)
|
||||
|
||||
func newLogger(name string, lev string) log.Logger {
|
||||
|
||||
@@ -11,9 +11,22 @@ func GetUniqueDashboardDatasourceUids(dashboard *simplejson.Json) []string {
|
||||
for _, panelObj := range dashboard.Get("panels").MustArray() {
|
||||
panel := simplejson.NewFromAny(panelObj)
|
||||
uid := panel.Get("datasource").Get("uid").MustString()
|
||||
if _, ok := exists[uid]; !ok {
|
||||
datasourceUids = append(datasourceUids, uid)
|
||||
exists[uid] = true
|
||||
|
||||
// if uid is for a mixed datasource, get the datasource uids from the targets
|
||||
if uid == "-- Mixed --" {
|
||||
for _, target := range panel.Get("targets").MustArray() {
|
||||
target := simplejson.NewFromAny(target)
|
||||
datasourceUid := target.Get("datasource").Get("uid").MustString()
|
||||
if _, ok := exists[datasourceUid]; !ok {
|
||||
datasourceUids = append(datasourceUids, datasourceUid)
|
||||
exists[datasourceUid] = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if _, ok := exists[uid]; !ok {
|
||||
datasourceUids = append(datasourceUids, uid)
|
||||
exists[uid] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +56,79 @@ const (
|
||||
"schemaVersion": 35
|
||||
}`
|
||||
|
||||
dashboardWithMixedDatasource = `
|
||||
{
|
||||
"panels": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "-- Mixed --"
|
||||
},
|
||||
"id": 1,
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "abc123"
|
||||
},
|
||||
"exemplar": true,
|
||||
"expr": "go_goroutines{job=\"$job\"}",
|
||||
"interval": "",
|
||||
"legendFormat": "",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Panel Title",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "_yxMP8Ynk"
|
||||
},
|
||||
"id": 2,
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "_yxMP8Ynk"
|
||||
},
|
||||
"exemplar": true,
|
||||
"expr": "go_goroutines{job=\"$job\"}",
|
||||
"interval": "",
|
||||
"legendFormat": "",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Panel Title",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "_yxMP8Ynk"
|
||||
},
|
||||
"id": 3,
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "_yxMP8Ynk"
|
||||
},
|
||||
"exemplar": true,
|
||||
"expr": "go_goroutines{job=\"$job\"}",
|
||||
"interval": "",
|
||||
"legendFormat": "",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Panel Title",
|
||||
"type": "timeseries"
|
||||
}
|
||||
],
|
||||
"schemaVersion": 35
|
||||
}`
|
||||
|
||||
dashboardWithDuplicateDatasources = `
|
||||
{
|
||||
"panels": [
|
||||
@@ -163,6 +236,16 @@ func TestGetUniqueDashboardDatasourceUids(t *testing.T) {
|
||||
require.Equal(t, "_yxMP8Ynk", uids[1])
|
||||
})
|
||||
|
||||
t.Run("can get unique datasource ids from dashboard with a mixed datasource", func(t *testing.T) {
|
||||
json, err := simplejson.NewJson([]byte(dashboardWithMixedDatasource))
|
||||
require.NoError(t, err)
|
||||
|
||||
uids := GetUniqueDashboardDatasourceUids(json)
|
||||
require.Len(t, uids, 2)
|
||||
require.Equal(t, "abc123", uids[0])
|
||||
require.Equal(t, "_yxMP8Ynk", uids[1])
|
||||
})
|
||||
|
||||
t.Run("can get no datasource uids from empty dashboard", func(t *testing.T) {
|
||||
json, err := simplejson.NewJson([]byte(`{"panels": {}}`))
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -102,6 +102,9 @@ type AddOrgUserCommand struct {
|
||||
|
||||
OrgId int64 `json:"-"`
|
||||
UserId int64 `json:"-"`
|
||||
|
||||
// internal use: avoid adding service accounts to orgs via user routes
|
||||
AllowAddingServiceAccount bool `json:"-"`
|
||||
}
|
||||
|
||||
type UpdateOrgUserCommand struct {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
)
|
||||
|
||||
const (
|
||||
ActionAppAccess = "plugins.app:access"
|
||||
)
|
||||
|
||||
var (
|
||||
ScopeProvider = ac.NewScopeProvider("plugins")
|
||||
)
|
||||
|
||||
func DeclareRBACRoles(acService ac.AccessControl) error {
|
||||
AppPluginsReader := ac.RoleRegistration{
|
||||
Role: ac.RoleDTO{
|
||||
Name: ac.FixedRolePrefix + "plugins.app:reader",
|
||||
DisplayName: "Application Plugins Access",
|
||||
Description: "Access application plugins (still enforcing the organization role)",
|
||||
Group: "Plugins",
|
||||
Permissions: []ac.Permission{
|
||||
{Action: ActionAppAccess, Scope: ScopeProvider.GetResourceAllScope()},
|
||||
},
|
||||
},
|
||||
Grants: []string{string(models.ROLE_VIEWER)},
|
||||
}
|
||||
return acService.DeclareFixedRoles(AppPluginsReader)
|
||||
}
|
||||
@@ -13,4 +13,4 @@ DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
|
||||
|
||||
cd "$DIR"
|
||||
|
||||
protoc -I ./ rendererv2.proto --go_out=plugins=grpc:./
|
||||
protoc -I ./ *.proto --go_out=plugins=grpc:./
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user