diff --git a/.betterer.results b/.betterer.results index 2638b8fecaf..7922ba7b01a 100644 --- a/.betterer.results +++ b/.betterer.results @@ -4881,7 +4881,7 @@ exports[`better eslint`] = { "public/app/plugins/panel/geomap/utils/selection.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], - "public/app/plugins/panel/geomap/utils/tootltip.ts:5381": [ + "public/app/plugins/panel/geomap/utils/tooltip.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], "public/app/plugins/panel/graph/GraphContextMenuCtrl.ts:5381": [ diff --git a/.drone.star b/.drone.star index 43d5d898ee2..39cb4b7fe5d 100644 --- a/.drone.star +++ b/.drone.star @@ -11,8 +11,6 @@ load("scripts/drone/events/pr.star", "pr_pipelines") load("scripts/drone/events/main.star", "main_pipelines") load( "scripts/drone/events/release.star", - "enterprise2_pipelines", - "enterprise_pipelines", "integration_test_pipelines", "oss_pipelines", "publish_artifacts_pipelines", @@ -33,8 +31,6 @@ load( "publish_ci_build_container_image_pipeline", "publish_ci_windows_test_image_pipeline", ) -load("scripts/drone/pipelines/github.star", "publish_github_pipeline") -load("scripts/drone/pipelines/aws_marketplace.star", "publish_aws_marketplace_pipeline") load( "scripts/drone/pipelines/windows.star", "windows_test_backend", @@ -48,13 +44,7 @@ def main(_ctx): pr_pipelines() + main_pipelines() + oss_pipelines() + - enterprise_pipelines() + - enterprise2_pipelines() + publish_image_pipelines_public() + - publish_github_pipeline("public") + - publish_github_pipeline("security") + - publish_aws_marketplace_pipeline("public") + - publish_artifacts_pipelines("security") + publish_artifacts_pipelines("public") + publish_npm_pipelines() + publish_packages_pipeline() + diff --git a/.drone.yml b/.drone.yml index 8ec6f3f3579..3ea7ad31d2e 100644 --- a/.drone.yml +++ b/.drone.yml @@ -159,7 +159,6 @@ steps: - yarn run ci:test-frontend depends_on: - yarn-install - - clone-enterprise environment: TEST_MAX_WORKERS: 50% image: grafana/build-container:1.7.5 @@ -233,7 +232,6 @@ steps: - yarn run typecheck depends_on: - yarn-install - - clone-enterprise environment: TEST_MAX_WORKERS: 50% image: grafana/build-container:1.7.5 @@ -323,8 +321,7 @@ steps: - '# The following command will fail if running code generators produces any diff in output.' - CODEGEN_VERIFY=1 make gen-cue - depends_on: - - clone-enterprise + depends_on: [] image: grafana/build-container:1.7.5 name: verify-gen-cue - commands: @@ -332,8 +329,7 @@ steps: - '# The following command will fail if running code generators produces any diff in output.' - CODEGEN_VERIFY=1 make gen-jsonnet - depends_on: - - clone-enterprise + depends_on: [] image: grafana/build-container:1.7.5 name: verify-gen-jsonnet - commands: @@ -427,8 +423,7 @@ steps: name: clone-enterprise - commands: - make gen-go - depends_on: - - clone-enterprise + depends_on: [] image: grafana/build-container:1.7.5 name: wire-install - commands: @@ -581,7 +576,7 @@ steps: image: grafana/build-container:1.7.5 name: build-backend - commands: - - ./bin/build build-frontend --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER} + - ./bin/build build-frontend --jobs 8 --edition oss--build-id ${DRONE_BUILD_NUMBER} depends_on: - compile-build-cmd - yarn-install @@ -590,7 +585,7 @@ steps: image: grafana/build-container:1.7.5 name: build-frontend - commands: - - ./bin/build build-frontend-packages --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER} + - ./bin/build build-frontend-packages --jobs 8 --edition oss--build-id ${DRONE_BUILD_NUMBER} - yarn packages:pack - ./scripts/validate-npm-packages.sh depends_on: @@ -920,8 +915,7 @@ steps: - '# The following command will fail if running code generators produces any diff in output.' - CODEGEN_VERIFY=1 make gen-cue - depends_on: - - clone-enterprise + depends_on: [] image: grafana/build-container:1.7.5 name: verify-gen-cue - commands: @@ -929,8 +923,7 @@ steps: - '# The following command will fail if running code generators produces any diff in output.' - CODEGEN_VERIFY=1 make gen-jsonnet - depends_on: - - clone-enterprise + depends_on: [] image: grafana/build-container:1.7.5 name: verify-gen-jsonnet - commands: @@ -1720,7 +1713,7 @@ steps: image: grafana/build-container:1.7.5 name: build-backend - commands: - - ./bin/build build-frontend --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER} + - ./bin/build build-frontend --jobs 8 --edition oss--build-id ${DRONE_BUILD_NUMBER} depends_on: - compile-build-cmd - yarn-install @@ -1729,7 +1722,7 @@ steps: image: grafana/build-container:1.7.5 name: build-frontend - commands: - - ./bin/build build-frontend-packages --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER} + - ./bin/build build-frontend-packages --jobs 8 --edition oss--build-id ${DRONE_BUILD_NUMBER} - yarn packages:pack - ./scripts/validate-npm-packages.sh depends_on: @@ -2303,29 +2296,6 @@ steps: -OutFile grabpl.exe image: grafana/ci-wix:0.1.1 name: windows-init -- commands: - - $$gcpKey = $$env:GCP_KEY - - '[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($$gcpKey)) - > gcpkey.json' - - dos2unix gcpkey.json - - gcloud auth activate-service-account --key-file=gcpkey.json - - rm gcpkey.json - - cp C:\App\nssm-2.24.zip . - - .\grabpl.exe windows-installer --edition oss --build-id $$env:DRONE_BUILD_NUMBER - - $$fname = ((Get-Childitem grafana*.msi -name) -split "`n")[0] - - gsutil cp $$fname gs://grafana-downloads/oss/main/ - - gsutil cp "$$fname.sha256" gs://grafana-downloads/oss/main/ - depends_on: - - windows-init - environment: - GCP_KEY: - from_secret: gcp_key - GITHUB_TOKEN: - from_secret: github_token - PRERELEASE_BUCKET: - from_secret: prerelease_bucket - image: grafana/ci-wix:0.1.1 - name: build-windows-installer trigger: branch: main event: @@ -2508,7 +2478,7 @@ environment: image_pull_secrets: - dockerconfigjson kind: pipeline -name: release-oss-build-e2e-publish +name: release-build-e2e-publish node: type: no-parallel platform: @@ -2589,7 +2559,7 @@ steps: image: grafana/build-container:1.7.5 name: build-plugins - commands: - - ./bin/build package --jobs 8 --edition oss --sign ${DRONE_TAG} + - ./bin/build package --jobs 8 --edition oss --sign $${DRONE_TAG} depends_on: - build-plugins - build-backend @@ -2811,7 +2781,7 @@ environment: image_pull_secrets: - dockerconfigjson kind: pipeline -name: release-oss-test-frontend +name: release-test-frontend node: type: no-parallel platform: @@ -2871,7 +2841,7 @@ environment: image_pull_secrets: - dockerconfigjson kind: pipeline -name: release-oss-test-backend +name: release-test-backend node: type: no-parallel platform: @@ -2945,14 +2915,14 @@ volumes: clone: retries: 3 depends_on: -- release-oss-build-e2e-publish -- release-oss-test-frontend +- release-build-e2e-publish +- release-test-frontend environment: EDITION: oss image_pull_secrets: - dockerconfigjson kind: pipeline -name: release-oss-windows +name: release-windows platform: arch: amd64 os: windows @@ -3007,854 +2977,6 @@ volumes: path: //./pipe/docker_engine/ name: docker --- -clone: - disable: true -depends_on: [] -environment: - EDITION: enterprise -image_pull_secrets: -- dockerconfigjson -kind: pipeline -name: release-enterprise-build-e2e-publish -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/v3.0.39/grabpl - - chmod +x bin/grabpl - image: byrnedo/alpine-curl:0.1.8 - name: grabpl -- commands: - - echo $DRONE_RUNNER_NAME - image: alpine:3.17.1 - name: identify-runner -- commands: - - git clone "https://$${GITHUB_TOKEN}@github.com/grafana/grafana-enterprise.git" - - cd grafana-enterprise - - git checkout ${DRONE_TAG} - environment: - GITHUB_TOKEN: - from_secret: github_token - image: grafana/build-container:1.7.5 - name: clone-enterprise -- commands: - - mv bin/grabpl /tmp/ - - rmdir bin - - mv grafana-enterprise /tmp/ - - /tmp/grabpl init-enterprise --github-token $${GITHUB_TOKEN} /tmp/grafana-enterprise - ${DRONE_TAG} - - mv /tmp/grafana-enterprise/deployment_tools_config.json deployment_tools_config.json - - mkdir bin - - mv /tmp/grabpl bin/ - depends_on: - - clone-enterprise - - grabpl - environment: - GITHUB_TOKEN: - from_secret: github_token - image: grafana/build-container:1.7.5 - name: init-enterprise -- commands: - - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd - depends_on: - - init-enterprise - environment: - CGO_ENABLED: 0 - image: golang:1.20.6 - name: compile-build-cmd -- commands: - - make gen-go - depends_on: - - init-enterprise - image: grafana/build-container:1.7.5 - name: wire-install -- commands: - - yarn install --immutable - depends_on: - - init-enterprise - image: grafana/build-container:1.7.5 - name: yarn-install -- commands: - - '# It is required that code generated from Thema/CUE be committed and in sync - with its inputs.' - - '# The following command will fail if running code generators produces any diff - in output.' - - CODEGEN_VERIFY=1 make gen-cue - depends_on: - - init-enterprise - image: grafana/build-container:1.7.5 - name: verify-gen-cue -- commands: - - '# It is required that generated jsonnet is committed and in sync with its inputs.' - - '# The following command will fail if running code generators produces any diff - in output.' - - CODEGEN_VERIFY=1 make gen-jsonnet - depends_on: - - init-enterprise - image: grafana/build-container:1.7.5 - name: verify-gen-jsonnet -- commands: - - ./bin/build build-backend --jobs 8 --edition enterprise ${DRONE_TAG} - depends_on: - - wire-install - - compile-build-cmd - image: grafana/build-container:1.7.5 - name: build-backend -- commands: - - ./bin/build build-frontend --jobs 8 --edition enterprise ${DRONE_TAG} - depends_on: - - compile-build-cmd - - yarn-install - environment: - NODE_OPTIONS: --max_old_space_size=8192 - image: grafana/build-container:1.7.5 - name: build-frontend -- commands: - - ./bin/build build-frontend-packages --jobs 8 --edition enterprise ${DRONE_TAG} - depends_on: - - compile-build-cmd - - yarn-install - environment: - NODE_OPTIONS: --max_old_space_size=8192 - image: grafana/build-container:1.7.5 - name: build-frontend-packages -- commands: - - ./bin/build build-plugins --jobs 8 --edition enterprise - depends_on: - - compile-build-cmd - - yarn-install - environment: - GRAFANA_API_KEY: - from_secret: grafana_api_key - image: grafana/build-container:1.7.5 - name: build-plugins -- commands: - - ./bin/build package --jobs 8 --edition enterprise --sign ${DRONE_TAG} - depends_on: - - build-plugins - - build-backend - - build-frontend - - build-frontend-packages - environment: - GPG_KEY_PASSWORD: - from_secret: packages_gpg_passphrase - GPG_PRIV_KEY: - from_secret: packages_gpg_private_key - GPG_PUB_KEY: - from_secret: packages_gpg_public_key - GRAFANA_API_KEY: - from_secret: grafana_api_key - image: grafana/build-container:1.7.5 - name: package -- commands: - - ls dist/*.tar.gz* - - cp dist/*.tar.gz* packaging/docker/ - depends_on: - - package - image: grafana/build-container:1.7.5 - name: copy-packages-for-docker -- commands: - - ./bin/build build-docker --edition enterprise --shouldSave - depends_on: - - copy-packages-for-docker - - compile-build-cmd - environment: - GCP_KEY: - from_secret: gcp_key - image: google/cloud-sdk:431.0.0 - name: build-docker-images - volumes: - - name: docker - path: /var/run/docker.sock -- commands: - - ./bin/build build-docker --edition enterprise --shouldSave --ubuntu - depends_on: - - copy-packages-for-docker - - compile-build-cmd - environment: - GCP_KEY: - from_secret: gcp_key - image: google/cloud-sdk:431.0.0 - name: build-docker-images-ubuntu - volumes: - - name: docker - path: /var/run/docker.sock -- commands: - - ./scripts/grafana-server/start-server - depends_on: - - build-plugins - - build-backend - - build-frontend - - build-frontend-packages - detach: true - environment: - ARCH: linux-amd64 - PORT: 3001 - RUNDIR: scripts/grafana-server/tmp-grafana-enterprise - image: grafana/build-container:1.7.5 - name: grafana-server -- commands: - - apt-get install -y netcat - - ./bin/build e2e-tests --port 3001 --suite dashboards-suite --tries 3 - depends_on: - - grafana-server - environment: - HOST: grafana-server - image: cypress/included:9.5.1-node16.14.0-slim-chrome99-ff97 - name: end-to-end-tests-dashboards-suite -- commands: - - apt-get install -y netcat - - ./bin/build e2e-tests --port 3001 --suite smoke-tests-suite --tries 3 - depends_on: - - grafana-server - environment: - HOST: grafana-server - image: cypress/included:9.5.1-node16.14.0-slim-chrome99-ff97 - name: end-to-end-tests-smoke-tests-suite -- commands: - - apt-get install -y netcat - - ./bin/build e2e-tests --port 3001 --suite panels-suite --tries 3 - depends_on: - - grafana-server - environment: - HOST: grafana-server - image: cypress/included:9.5.1-node16.14.0-slim-chrome99-ff97 - name: end-to-end-tests-panels-suite -- commands: - - apt-get install -y netcat - - ./bin/build e2e-tests --port 3001 --suite various-suite --tries 3 - depends_on: - - grafana-server - environment: - HOST: grafana-server - image: cypress/included:9.5.1-node16.14.0-slim-chrome99-ff97 - name: end-to-end-tests-various-suite -- commands: - - apt-get update - - apt-get install -yq zip - - printenv GCP_GRAFANA_UPLOAD_ARTIFACTS_KEY > /tmp/gcpkey_upload_artifacts.json - - gcloud auth activate-service-account --key-file=/tmp/gcpkey_upload_artifacts.json - - find ./e2e -type f -name "*spec.ts.mp4" | zip e2e/videos.zip -@ - - gsutil cp e2e/videos.zip gs://$${E2E_TEST_ARTIFACTS_BUCKET}/${DRONE_BUILD_NUMBER}/artifacts/videos/videos.zip - - export E2E_ARTIFACTS_VIDEO_ZIP=https://storage.googleapis.com/$${E2E_TEST_ARTIFACTS_BUCKET}/${DRONE_BUILD_NUMBER}/artifacts/videos/videos.zip - - 'echo "E2E Test artifacts uploaded to: $${E2E_ARTIFACTS_VIDEO_ZIP}"' - - 'curl -X POST https://api.github.com/repos/${DRONE_REPO}/statuses/${DRONE_COMMIT_SHA} - -H "Authorization: token $${GITHUB_TOKEN}" -d "{\"state\":\"success\",\"target_url\":\"$${E2E_ARTIFACTS_VIDEO_ZIP}\", - \"description\": \"Click on the details to download e2e recording videos\", \"context\": - \"e2e_artifacts\"}"' - depends_on: - - end-to-end-tests-dashboards-suite - - end-to-end-tests-panels-suite - - end-to-end-tests-smoke-tests-suite - - end-to-end-tests-various-suite - environment: - E2E_TEST_ARTIFACTS_BUCKET: releng-pipeline-artifacts-dev - GCP_GRAFANA_UPLOAD_ARTIFACTS_KEY: - from_secret: gcp_upload_artifacts_key - GITHUB_TOKEN: - from_secret: github_token - failure: ignore - image: google/cloud-sdk:431.0.0 - name: e2e-tests-artifacts-upload - when: - status: - - success - - failure -- commands: - - ./bin/build upload-cdn --edition enterprise - depends_on: - - package - environment: - GCP_KEY: - from_secret: gcp_key - PRERELEASE_BUCKET: - from_secret: prerelease_bucket - image: grafana/grafana-ci-deploy:1.3.3 - name: upload-cdn-assets -- commands: - - ./bin/build upload-packages --edition enterprise - depends_on: - - package - environment: - GCP_KEY: - from_secret: gcp_key - PRERELEASE_BUCKET: - from_secret: prerelease_bucket - image: grafana/grafana-ci-deploy:1.3.3 - name: upload-packages -trigger: - event: - exclude: - - promote - ref: - exclude: - - refs/tags/*-cloud* - include: - - refs/tags/v* -type: docker -volumes: -- host: - path: /var/run/docker.sock - name: docker ---- -clone: - disable: true -depends_on: [] -environment: - EDITION: enterprise -image_pull_secrets: -- dockerconfigjson -kind: pipeline -name: release-enterprise-test-frontend -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/v3.0.39/grabpl - - chmod +x bin/grabpl - image: byrnedo/alpine-curl:0.1.8 - name: grabpl -- commands: - - git clone "https://$${GITHUB_TOKEN}@github.com/grafana/grafana-enterprise.git" - - cd grafana-enterprise - - git checkout ${DRONE_TAG} - environment: - GITHUB_TOKEN: - from_secret: github_token - image: grafana/build-container:1.7.5 - name: clone-enterprise -- commands: - - mv bin/grabpl /tmp/ - - rmdir bin - - mv grafana-enterprise /tmp/ - - /tmp/grabpl init-enterprise --github-token $${GITHUB_TOKEN} /tmp/grafana-enterprise - ${DRONE_TAG} - - mv /tmp/grafana-enterprise/deployment_tools_config.json deployment_tools_config.json - - mkdir bin - - mv /tmp/grabpl bin/ - depends_on: - - clone-enterprise - - grabpl - environment: - GITHUB_TOKEN: - from_secret: github_token - image: grafana/build-container:1.7.5 - name: init-enterprise -- commands: - - echo $DRONE_RUNNER_NAME - image: alpine:3.17.1 - name: identify-runner -- commands: - - yarn install --immutable - depends_on: - - init-enterprise - image: grafana/build-container:1.7.5 - name: yarn-install -- commands: - - yarn betterer ci - depends_on: - - init-enterprise - - yarn-install - image: grafana/build-container:1.7.5 - name: betterer-frontend -- commands: - - yarn run ci:test-frontend - depends_on: - - init-enterprise - - yarn-install - environment: - TEST_MAX_WORKERS: 50% - image: grafana/build-container:1.7.5 - name: test-frontend -trigger: - event: - exclude: - - promote - ref: - exclude: - - refs/tags/*-cloud* - include: - - refs/tags/v* -type: docker -volumes: -- host: - path: /var/run/docker.sock - name: docker ---- -clone: - disable: true -depends_on: [] -environment: - EDITION: enterprise -image_pull_secrets: -- dockerconfigjson -kind: pipeline -name: release-enterprise-test-backend -node: - type: no-parallel -platform: - arch: amd64 - os: linux -services: [] -steps: -- commands: - - git clone "https://$${GITHUB_TOKEN}@github.com/grafana/grafana-enterprise.git" - - cd grafana-enterprise - - git checkout ${DRONE_TAG} - environment: - GITHUB_TOKEN: - from_secret: github_token - image: grafana/build-container:1.7.5 - name: clone-enterprise -- commands: - - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.39/grabpl - - chmod +x bin/grabpl - image: byrnedo/alpine-curl:0.1.8 - name: grabpl -- commands: - - mv bin/grabpl /tmp/ - - rmdir bin - - mv grafana-enterprise /tmp/ - - /tmp/grabpl init-enterprise --github-token $${GITHUB_TOKEN} /tmp/grafana-enterprise - ${DRONE_TAG} - - mv /tmp/grafana-enterprise/deployment_tools_config.json deployment_tools_config.json - - mkdir bin - - mv /tmp/grabpl bin/ - depends_on: - - clone-enterprise - - grabpl - environment: - GITHUB_TOKEN: - from_secret: github_token - image: grafana/build-container:1.7.5 - name: init-enterprise -- commands: - - echo $DRONE_RUNNER_NAME - image: alpine:3.17.1 - name: identify-runner -- commands: - - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd - depends_on: - - init-enterprise - environment: - CGO_ENABLED: 0 - image: golang:1.20.6 - name: compile-build-cmd -- commands: - - '# It is required that code generated from Thema/CUE be committed and in sync - with its inputs.' - - '# The following command will fail if running code generators produces any diff - in output.' - - CODEGEN_VERIFY=1 make gen-cue - depends_on: - - init-enterprise - image: grafana/build-container:1.7.5 - name: verify-gen-cue -- commands: - - '# It is required that generated jsonnet is committed and in sync with its inputs.' - - '# The following command will fail if running code generators produces any diff - in output.' - - CODEGEN_VERIFY=1 make gen-jsonnet - depends_on: - - init-enterprise - image: grafana/build-container:1.7.5 - name: verify-gen-jsonnet -- commands: - - make gen-go - depends_on: - - verify-gen-cue - image: grafana/build-container:1.7.5 - name: wire-install -- commands: - - go test -tags requires_buildifer -short -covermode=atomic -timeout=5m ./pkg/... - depends_on: - - wire-install - image: grafana/build-container:1.7.5 - name: test-backend -- commands: - - go test -count=1 -covermode=atomic -timeout=5m -run '^TestIntegration' $(find - ./pkg -type f -name '*_test.go' -exec grep -l '^func TestIntegration' '{}' '+' - | grep -o '\(.*\)/' | sort -u) - depends_on: - - wire-install - image: grafana/build-container:1.7.5 - name: test-backend-integration -trigger: - event: - exclude: - - promote - ref: - exclude: - - refs/tags/*-cloud* - include: - - refs/tags/v* -type: docker -volumes: -- host: - path: /var/run/docker.sock - name: docker ---- -clone: - disable: true -depends_on: -- release-enterprise-build-e2e-publish -- release-enterprise-test-frontend -- release-enterprise-test-backend -environment: - EDITION: enterprise -image_pull_secrets: -- dockerconfigjson -kind: pipeline -name: release-enterprise-windows -platform: - arch: amd64 - os: windows - version: "1809" -services: [] -steps: -- commands: - - echo $env:DRONE_RUNNER_NAME - image: mcr.microsoft.com/windows:1809 - name: identify-runner -- commands: - - $$ProgressPreference = "SilentlyContinue" - - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.39/windows/grabpl.exe - -OutFile grabpl.exe - - git clone "https://$$env:GITHUB_TOKEN@github.com/grafana/grafana-enterprise.git" - - cd grafana-enterprise - - git checkout ${DRONE_TAG} - environment: - GITHUB_TOKEN: - from_secret: github_token - image: grafana/ci-wix:0.1.1 - name: clone -- commands: - - cp -r grafana-enterprise C:\App\grafana-enterprise - - rm -r -force grafana-enterprise - - cp grabpl.exe C:\App\grabpl.exe - - rm -force grabpl.exe - - C:\App\grabpl.exe init-enterprise --github-token $$env:GITHUB_TOKEN C:\App\grafana-enterprise - ${DRONE_TAG} - - cp C:\App\grabpl.exe grabpl.exe - depends_on: - - clone - environment: - GITHUB_TOKEN: - from_secret: github_token - image: grafana/ci-wix:0.1.1 - name: windows-init -- commands: - - $$gcpKey = $$env:GCP_KEY - - '[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($$gcpKey)) - > gcpkey.json' - - dos2unix gcpkey.json - - gcloud auth activate-service-account --key-file=gcpkey.json - - rm gcpkey.json - - cp C:\App\nssm-2.24.zip . - - .\grabpl.exe windows-installer --edition enterprise ${DRONE_TAG} - - $$fname = ((Get-Childitem grafana*.msi -name) -split "`n")[0] - - gsutil cp $$fname gs://%PRERELEASE_BUCKET%/artifacts/downloads/${DRONE_TAG}/enterprise/release/ - - gsutil cp "$$fname.sha256" gs://%PRERELEASE_BUCKET%/artifacts/downloads/${DRONE_TAG}/enterprise/release/ - depends_on: - - windows-init - environment: - GCP_KEY: - from_secret: gcp_key - GITHUB_TOKEN: - from_secret: github_token - PRERELEASE_BUCKET: - from_secret: prerelease_bucket - image: grafana/ci-wix:0.1.1 - name: build-windows-installer -trigger: - event: - exclude: - - promote - ref: - exclude: - - refs/tags/*-cloud* - include: - - refs/tags/v* -type: docker -volumes: -- host: - path: //./pipe/docker_engine/ - name: docker ---- -clone: - disable: true -depends_on: [] -environment: - EDITION: enterprise2 -image_pull_secrets: -- dockerconfigjson -kind: pipeline -name: release-enterprise2-build-e2e-publish -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/v3.0.39/grabpl - - chmod +x bin/grabpl - image: byrnedo/alpine-curl:0.1.8 - name: grabpl -- commands: - - echo $DRONE_RUNNER_NAME - image: alpine:3.17.1 - name: identify-runner -- commands: - - git clone "https://$${GITHUB_TOKEN}@github.com/grafana/grafana-enterprise.git" - - cd grafana-enterprise - - git checkout ${DRONE_TAG} - environment: - GITHUB_TOKEN: - from_secret: github_token - image: grafana/build-container:1.7.5 - name: clone-enterprise -- commands: - - mv bin/grabpl /tmp/ - - rmdir bin - - mv grafana-enterprise /tmp/ - - /tmp/grabpl init-enterprise --github-token $${GITHUB_TOKEN} /tmp/grafana-enterprise - ${DRONE_TAG} - - mv /tmp/grafana-enterprise/deployment_tools_config.json deployment_tools_config.json - - mkdir bin - - mv /tmp/grabpl bin/ - depends_on: - - clone-enterprise - - grabpl - environment: - GITHUB_TOKEN: - from_secret: github_token - image: grafana/build-container:1.7.5 - name: init-enterprise -- commands: - - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd - depends_on: - - init-enterprise - environment: - CGO_ENABLED: 0 - image: golang:1.20.6 - name: compile-build-cmd -- commands: - - make gen-go - depends_on: - - init-enterprise - image: grafana/build-container:1.7.5 - name: wire-install -- commands: - - yarn install --immutable - depends_on: - - init-enterprise - image: grafana/build-container:1.7.5 - name: yarn-install -- commands: - - '# It is required that code generated from Thema/CUE be committed and in sync - with its inputs.' - - '# The following command will fail if running code generators produces any diff - in output.' - - CODEGEN_VERIFY=1 make gen-cue - depends_on: - - init-enterprise - image: grafana/build-container:1.7.5 - name: verify-gen-cue -- commands: - - ./bin/build build-frontend --jobs 8 --edition enterprise ${DRONE_TAG} - depends_on: - - compile-build-cmd - - yarn-install - environment: - NODE_OPTIONS: --max_old_space_size=8192 - image: grafana/build-container:1.7.5 - name: build-frontend -- commands: - - ./bin/build build-frontend-packages --jobs 8 --edition enterprise ${DRONE_TAG} - depends_on: - - compile-build-cmd - - yarn-install - environment: - NODE_OPTIONS: --max_old_space_size=8192 - image: grafana/build-container:1.7.5 - name: build-frontend-packages -- commands: - - ./bin/build build-plugins --jobs 8 --edition enterprise - depends_on: - - compile-build-cmd - - yarn-install - environment: - GRAFANA_API_KEY: - from_secret: grafana_api_key - image: grafana/build-container:1.7.5 - name: build-plugins -- commands: - - ./bin/build build-backend --jobs 8 --edition enterprise2 ${DRONE_TAG} - depends_on: - - wire-install - - compile-build-cmd - image: grafana/build-container:1.7.5 - name: build-backend-enterprise2 -- commands: - - ./bin/build package --jobs 8 --edition enterprise2 --sign ${DRONE_TAG} - depends_on: - - build-plugins - - build-backend-enterprise2 - - build-frontend - - build-frontend-packages - environment: - GPG_KEY_PASSWORD: - from_secret: packages_gpg_passphrase - GPG_PRIV_KEY: - from_secret: packages_gpg_private_key - GPG_PUB_KEY: - from_secret: packages_gpg_public_key - GRAFANA_API_KEY: - from_secret: grafana_api_key - image: grafana/build-container:1.7.5 - name: package-enterprise2 -- commands: - - ./bin/build upload-cdn --edition enterprise2 - depends_on: - - package-enterprise2 - environment: - ENTERPRISE2_CDN_PATH: - from_secret: enterprise2-cdn-path - GCP_KEY: - from_secret: gcp_key - PRERELEASE_BUCKET: - from_secret: prerelease_bucket - image: grafana/grafana-ci-deploy:1.3.3 - name: upload-cdn-assets-enterprise2 -- commands: - - ls dist/*.tar.gz* - - cp dist/*.tar.gz* packaging/docker/ - depends_on: - - package-enterprise2 - image: grafana/build-container:1.7.5 - name: copy-packages-for-docker -- commands: - - ./bin/build build-docker --edition enterprise2 --shouldSave - depends_on: - - copy-packages-for-docker - - compile-build-cmd - environment: - DOCKER_ENTERPRISE2_REPO: - from_secret: docker_enterprise2_repo - GCP_KEY: - from_secret: gcp_key - image: google/cloud-sdk:431.0.0 - name: build-docker-images - volumes: - - name: docker - path: /var/run/docker.sock -- commands: - - ./bin/build build-docker --edition enterprise2 --shouldSave --ubuntu - depends_on: - - copy-packages-for-docker - - compile-build-cmd - environment: - DOCKER_ENTERPRISE2_REPO: - from_secret: docker_enterprise2_repo - GCP_KEY: - from_secret: gcp_key - image: google/cloud-sdk:431.0.0 - name: build-docker-images-ubuntu - volumes: - - name: docker - path: /var/run/docker.sock -- commands: - - ./bin/build artifacts docker fetch --edition enterprise2 - depends_on: - - build-docker-images - - build-docker-images-ubuntu - environment: - DOCKER_ENTERPRISE2_REPO: - from_secret: docker_enterprise2_repo - DOCKER_PASSWORD: - from_secret: docker_password - DOCKER_USER: - from_secret: docker_username - GCP_KEY: - from_secret: gcp_key - image: google/cloud-sdk:431.0.0 - name: fetch-images-enterprise2 - volumes: - - name: docker - path: /var/run/docker.sock -- commands: - - ./bin/build artifacts docker publish-enterprise2 --dockerhub-repo $${DOCKER_ENTERPRISE2_REPO} - depends_on: - - fetch-images-enterprise2 - environment: - DOCKER_ENTERPRISE2_REPO: - from_secret: docker_enterprise2_repo - DOCKER_PASSWORD: - from_secret: docker_password - DOCKER_USER: - from_secret: docker_username - GCP_KEY: - from_secret: gcp_key_hg - GITHUB_APP_ID: - from_secret: delivery-bot-app-id - GITHUB_APP_INSTALLATION_ID: - from_secret: delivery-bot-app-installation-id - GITHUB_APP_PRIVATE_KEY: - from_secret: delivery-bot-app-private-key - image: google/cloud-sdk:431.0.0 - name: publish-images-enterprise2 - volumes: - - name: docker - path: /var/run/docker.sock -- commands: - - ./bin/build upload-packages --edition enterprise2 - depends_on: - - package-enterprise2 - environment: - GCP_KEY: - from_secret: gcp_key - PRERELEASE_BUCKET: - from_secret: prerelease_bucket - image: grafana/grafana-ci-deploy:1.3.3 - name: upload-packages-enterprise2 -trigger: - event: - exclude: - - promote - ref: - exclude: - - refs/tags/*-cloud* - include: - - refs/tags/v* -type: docker -volumes: -- host: - path: /var/run/docker.sock - name: docker -- name: postgres - temp: - medium: memory -- name: mysql57 - temp: - medium: memory -- name: mysql80 - temp: - medium: memory ---- clone: retries: 3 depends_on: [] @@ -3863,7 +2985,7 @@ environment: image_pull_secrets: - dockerconfigjson kind: pipeline -name: publish-docker-oss-public +name: publish-docker-public node: type: no-parallel platform: @@ -3893,8 +3015,6 @@ steps: depends_on: - compile-build-cmd environment: - DOCKER_ENTERPRISE2_REPO: - from_secret: docker_enterprise2_repo DOCKER_PASSWORD: from_secret: docker_password DOCKER_USER: @@ -3902,7 +3022,7 @@ steps: GCP_KEY: from_secret: gcp_key image: google/cloud-sdk:431.0.0 - name: fetch-images-oss + name: fetch-images volumes: - name: docker path: /var/run/docker.sock @@ -3910,7 +3030,7 @@ steps: - ./bin/grabpl artifacts docker publish --dockerhub-repo grafana/grafana --version-tag ${DRONE_TAG} depends_on: - - fetch-images-oss + - fetch-images environment: DOCKER_PASSWORD: from_secret: docker_password @@ -3933,7 +3053,7 @@ steps: - ./bin/grabpl artifacts docker publish --dockerhub-repo grafana/grafana-oss --version-tag ${DRONE_TAG} depends_on: - - fetch-images-oss + - fetch-images environment: DOCKER_PASSWORD: from_secret: docker_password @@ -3967,339 +3087,7 @@ clone: retries: 3 depends_on: [] environment: - EDITION: enterprise -image_pull_secrets: -- dockerconfigjson -kind: pipeline -name: publish-docker-enterprise-public -node: - type: no-parallel -platform: - arch: amd64 - os: linux -services: [] -steps: -- commands: - - echo $DRONE_RUNNER_NAME - image: alpine:3.17.1 - name: identify-runner -- commands: - - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.39/grabpl - - chmod +x bin/grabpl - image: byrnedo/alpine-curl:0.1.8 - name: grabpl -- commands: - - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd - depends_on: [] - environment: - CGO_ENABLED: 0 - image: golang:1.20.6 - name: compile-build-cmd -- commands: - - ./bin/build artifacts docker fetch --edition enterprise - depends_on: - - compile-build-cmd - environment: - DOCKER_ENTERPRISE2_REPO: - from_secret: docker_enterprise2_repo - DOCKER_PASSWORD: - from_secret: docker_password - DOCKER_USER: - from_secret: docker_username - GCP_KEY: - from_secret: gcp_key - image: google/cloud-sdk:431.0.0 - name: fetch-images-enterprise - volumes: - - name: docker - path: /var/run/docker.sock -- commands: - - ./bin/grabpl artifacts docker publish --dockerhub-repo grafana/grafana-enterprise - --version-tag ${DRONE_TAG} - depends_on: - - fetch-images-enterprise - environment: - DOCKER_PASSWORD: - from_secret: docker_password - DOCKER_USER: - from_secret: docker_username - GCP_KEY: - from_secret: gcp_key - GITHUB_APP_ID: - from_secret: delivery-bot-app-id - GITHUB_APP_INSTALLATION_ID: - from_secret: delivery-bot-app-installation-id - GITHUB_APP_PRIVATE_KEY: - from_secret: delivery-bot-app-private-key - image: google/cloud-sdk:431.0.0 - name: publish-images-grafana-enterprise - volumes: - - name: docker - path: /var/run/docker.sock -trigger: - event: - - promote - target: - - public -type: docker -volumes: -- host: - path: /var/run/docker.sock - name: docker ---- -clone: - retries: 3 -depends_on: [] -environment: - EDITION: enterprise2 -image_pull_secrets: -- dockerconfigjson -kind: pipeline -name: publish-github-public -node: - type: no-parallel -platform: - arch: amd64 - os: linux -services: [] -steps: -- commands: - - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd - depends_on: [] - environment: - CGO_ENABLED: 0 - image: golang:1.20.6 - name: compile-build-cmd -- commands: - - ./bin/build artifacts docker fetch --edition enterprise2 - depends_on: - - compile-build-cmd - environment: - DOCKER_ENTERPRISE2_REPO: - from_secret: docker_enterprise2_repo - DOCKER_PASSWORD: - from_secret: docker_password - DOCKER_USER: - from_secret: docker_username - GCP_KEY: - from_secret: gcp_key - image: google/cloud-sdk:431.0.0 - name: fetch-images-enterprise2 - volumes: - - name: docker - path: /var/run/docker.sock -- commands: - - ./bin/build publish github --repo $${GH_REGISTRY} --create - depends_on: - - fetch-images-enterprise2 - environment: - GH_REGISTRY: - from_secret: gh_registry - GH_TOKEN: - from_secret: github_token - image: grafana/grafana-ci-deploy:1.3.3 - name: publish-github -trigger: - event: - - promote - target: - - public -type: docker -volumes: -- host: - path: /var/run/docker.sock - name: docker ---- -clone: - retries: 3 -depends_on: [] -environment: - EDITION: enterprise2 -image_pull_secrets: -- dockerconfigjson -kind: pipeline -name: publish-github-security -node: - type: no-parallel -platform: - arch: amd64 - os: linux -services: [] -steps: -- commands: - - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd - depends_on: [] - environment: - CGO_ENABLED: 0 - image: golang:1.20.6 - name: compile-build-cmd -- commands: - - ./bin/build artifacts docker fetch --edition enterprise2 - depends_on: - - compile-build-cmd - environment: - DOCKER_ENTERPRISE2_REPO: - from_secret: docker_enterprise2_repo - DOCKER_PASSWORD: - from_secret: docker_password - DOCKER_USER: - from_secret: docker_username - GCP_KEY: - from_secret: gcp_key - image: google/cloud-sdk:431.0.0 - name: fetch-images-enterprise2 - volumes: - - name: docker - path: /var/run/docker.sock -- commands: - - ./bin/build publish github --repo $${GH_REGISTRY} --create - depends_on: - - fetch-images-enterprise2 - environment: - GH_REGISTRY: - from_secret: gh_registry - GH_TOKEN: - from_secret: github_token - image: grafana/grafana-ci-deploy:1.3.3 - name: publish-github -trigger: - event: - - promote - target: - - security -type: docker -volumes: -- host: - path: /var/run/docker.sock - name: docker ---- -clone: - retries: 3 -depends_on: -- publish-docker-enterprise-public -environment: - EDITION: enterprise2 -image_pull_secrets: -- dockerconfigjson -kind: pipeline -name: publish-aws-marketplace-public -node: - type: no-parallel -platform: - arch: amd64 - os: linux -services: [] -steps: -- commands: - - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd - depends_on: [] - environment: - CGO_ENABLED: 0 - image: golang:1.20.6 - name: compile-build-cmd -- commands: - - ./bin/build publish aws --image grafana/grafana-enterprise --repo grafana-labs/grafanaenterprise - --product 422b46fb-bea6-4f27-8bcc-832117bd627e - depends_on: - - compile-build-cmd - environment: - AWS_ACCESS_KEY_ID: - from_secret: aws_access_key_id - AWS_REGION: - from_secret: aws_region - AWS_SECRET_ACCESS_KEY: - from_secret: aws_secret_access_key - image: grafana/grafana-ci-deploy:1.3.3 - name: publish-aws-marketplace - volumes: - - name: docker - path: /var/run/docker.sock -trigger: - event: - - promote - target: - - public -type: docker -volumes: -- host: - path: /var/run/docker.sock - name: docker ---- -clone: - retries: 3 -depends_on: [] -environment: - EDITION: all -image_pull_secrets: -- dockerconfigjson -kind: pipeline -name: publish-artifacts-security -node: - type: no-parallel -platform: - arch: amd64 - os: linux -services: [] -steps: -- commands: - - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd - depends_on: [] - environment: - CGO_ENABLED: 0 - image: golang:1.20.6 - name: compile-build-cmd -- commands: - - ./bin/build artifacts packages --tag $${DRONE_TAG} --src-bucket $${PRERELEASE_BUCKET} - depends_on: - - compile-build-cmd - environment: - GCP_KEY: - from_secret: gcp_key - PRERELEASE_BUCKET: - from_secret: prerelease_bucket - image: grafana/grafana-ci-deploy:1.3.3 - name: publish-artifacts -- commands: - - ./bin/build artifacts static-assets --tag ${DRONE_TAG} - depends_on: - - compile-build-cmd - environment: - GCP_KEY: - from_secret: gcp_key - PRERELEASE_BUCKET: - from_secret: prerelease_bucket - STATIC_ASSET_EDITIONS: - from_secret: static_asset_editions - image: grafana/grafana-ci-deploy:1.3.3 - name: publish-static-assets -- commands: - - ./bin/build artifacts storybook --tag ${DRONE_TAG} - depends_on: - - compile-build-cmd - environment: - GCP_KEY: - from_secret: gcp_key - PRERELEASE_BUCKET: - from_secret: prerelease_bucket - image: grafana/grafana-ci-deploy:1.3.3 - name: publish-storybook -trigger: - event: - - promote - target: - - security -type: docker -volumes: -- host: - path: /var/run/docker.sock - name: docker ---- -clone: - retries: 3 -depends_on: [] -environment: - EDITION: all + EDITION: oss image_pull_secrets: - dockerconfigjson kind: pipeline @@ -4330,7 +3118,7 @@ steps: image: grafana/grafana-ci-deploy:1.3.3 name: publish-artifacts - commands: - - ./bin/build artifacts static-assets --tag ${DRONE_TAG} + - ./bin/build artifacts static-assets --tag ${DRONE_TAG} --static-asset-editions=grafana-oss depends_on: - compile-build-cmd environment: @@ -4368,7 +3156,7 @@ clone: retries: 3 depends_on: [] environment: - EDITION: all + EDITION: oss image_pull_secrets: - dockerconfigjson kind: pipeline @@ -4431,14 +3219,13 @@ clone: retries: 3 depends_on: - publish-artifacts-public -- publish-docker-oss-public -- publish-docker-enterprise-public +- publish-docker-public environment: EDITION: oss image_pull_secrets: - dockerconfigjson kind: pipeline -name: publish-packages-oss +name: publish-packages node: type: no-parallel platform: @@ -4468,7 +3255,7 @@ steps: from_secret: packages_gpg_private_key gpg_public_key: from_secret: packages_gpg_public_key - package_path: gs://grafana-prerelease/artifacts/downloads/*${DRONE_TAG}/oss/**.deb + package_path: gs://grafana-prerelease/artifacts/downloads/*$${DRONE_TAG}/oss/**.deb secret_access_key: from_secret: packages_secret_access_key service_account_json: @@ -4489,7 +3276,7 @@ steps: from_secret: packages_gpg_private_key gpg_public_key: from_secret: packages_gpg_public_key - package_path: gs://grafana-prerelease/artifacts/downloads/*${DRONE_TAG}/oss/**.rpm + package_path: gs://grafana-prerelease/artifacts/downloads/*$${DRONE_TAG}/oss/**.rpm secret_access_key: from_secret: packages_secret_access_key service_account_json: @@ -4506,7 +3293,7 @@ steps: GRAFANA_COM_API_KEY: from_secret: grafana_api_key image: grafana/grafana-ci-deploy:1.3.3 - name: publish-grafanacom-oss + name: publish-grafanacom trigger: event: - promote @@ -4521,102 +3308,8 @@ volumes: clone: retries: 3 depends_on: -- publish-artifacts-public -- publish-docker-oss-public -- publish-docker-enterprise-public -environment: - EDITION: enterprise -image_pull_secrets: -- dockerconfigjson -kind: pipeline -name: publish-packages-enterprise -node: - type: no-parallel -platform: - arch: amd64 - os: linux -services: [] -steps: -- commands: - - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd - depends_on: [] - environment: - CGO_ENABLED: 0 - image: golang:1.20.6 - name: compile-build-cmd -- depends_on: - - compile-build-cmd - image: us.gcr.io/kubernetes-dev/package-publish:latest - name: publish-linux-packages-deb - privileged: true - settings: - access_key_id: - from_secret: packages_access_key_id - deb_distribution: auto - gpg_passphrase: - from_secret: packages_gpg_passphrase - gpg_private_key: - from_secret: packages_gpg_private_key - gpg_public_key: - from_secret: packages_gpg_public_key - package_path: gs://grafana-prerelease/artifacts/downloads/*${DRONE_TAG}/enterprise/**.deb - secret_access_key: - from_secret: packages_secret_access_key - service_account_json: - from_secret: packages_service_account - target_bucket: grafana-packages -- depends_on: - - compile-build-cmd - image: us.gcr.io/kubernetes-dev/package-publish:latest - name: publish-linux-packages-rpm - privileged: true - settings: - access_key_id: - from_secret: packages_access_key_id - deb_distribution: auto - gpg_passphrase: - from_secret: packages_gpg_passphrase - gpg_private_key: - from_secret: packages_gpg_private_key - gpg_public_key: - from_secret: packages_gpg_public_key - package_path: gs://grafana-prerelease/artifacts/downloads/*${DRONE_TAG}/enterprise/**.rpm - secret_access_key: - from_secret: packages_secret_access_key - service_account_json: - from_secret: packages_service_account - target_bucket: grafana-packages -- commands: - - ./bin/build publish grafana-com --edition enterprise ${DRONE_TAG} - depends_on: - - publish-linux-packages-deb - - publish-linux-packages-rpm - environment: - GCP_KEY: - from_secret: gcp_key - GRAFANA_COM_API_KEY: - from_secret: grafana_api_key - image: grafana/grafana-ci-deploy:1.3.3 - name: publish-grafanacom-enterprise -trigger: - event: - - promote - target: - - public -type: docker -volumes: -- host: - path: /var/run/docker.sock - name: docker ---- -clone: - retries: 3 -depends_on: -- release-oss-build-e2e-publish -- release-enterprise-build-e2e-publish -- release-enterprise2-build-e2e-publish -- release-oss-windows -- release-enterprise-windows +- release-build-e2e-publish +- release-windows image_pull_secrets: - dockerconfigjson kind: pipeline @@ -4819,7 +3512,7 @@ environment: image_pull_secrets: - dockerconfigjson kind: pipeline -name: testing-oss-test-backend-windows +name: testing-test-backend-windows platform: arch: amd64 os: windows @@ -4871,7 +3564,7 @@ environment: image_pull_secrets: - dockerconfigjson kind: pipeline -name: release-branch-oss-build-e2e-publish +name: release-branch-build-e2e-publish node: type: no-parallel platform: @@ -4924,7 +3617,7 @@ steps: image: grafana/build-container:1.7.5 name: build-backend - commands: - - ./bin/build build-frontend --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER} + - ./bin/build build-frontend --jobs 8 --edition oss--build-id ${DRONE_BUILD_NUMBER} depends_on: - compile-build-cmd - yarn-install @@ -4933,7 +3626,7 @@ steps: image: grafana/build-container:1.7.5 name: build-frontend - commands: - - ./bin/build build-frontend-packages --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER} + - ./bin/build build-frontend-packages --jobs 8 --edition oss--build-id ${DRONE_BUILD_NUMBER} - yarn packages:pack - ./scripts/validate-npm-packages.sh depends_on: @@ -5155,7 +3848,7 @@ environment: image_pull_secrets: - dockerconfigjson kind: pipeline -name: release-branch-oss-test-frontend +name: release-branch-test-frontend node: type: no-parallel platform: @@ -5209,7 +3902,7 @@ environment: image_pull_secrets: - dockerconfigjson kind: pipeline -name: release-branch-oss-test-backend +name: release-branch-test-backend node: type: no-parallel platform: @@ -5282,7 +3975,7 @@ environment: image_pull_secrets: - dockerconfigjson kind: pipeline -name: release-branch-oss-integration-tests +name: release-branch-integration-tests node: type: no-parallel platform: @@ -5455,14 +4148,14 @@ volumes: clone: retries: 3 depends_on: -- release-branch-oss-build-e2e-publish -- release-branch-oss-test-frontend +- release-branch-build-e2e-publish +- release-branch-test-frontend environment: EDITION: oss image_pull_secrets: - dockerconfigjson kind: pipeline -name: release-branch-oss-windows +name: release-branch-windows platform: arch: amd64 os: windows @@ -5507,1043 +4200,6 @@ volumes: path: //./pipe/docker_engine/ name: docker --- -clone: - disable: true -depends_on: [] -environment: - EDITION: enterprise -image_pull_secrets: -- dockerconfigjson -kind: pipeline -name: release-branch-enterprise-build-e2e-publish -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/v3.0.39/grabpl - - chmod +x bin/grabpl - image: byrnedo/alpine-curl:0.1.8 - name: grabpl -- commands: - - echo $DRONE_RUNNER_NAME - image: alpine:3.17.1 - name: identify-runner -- commands: - - git clone "https://$${GITHUB_TOKEN}@github.com/grafana/grafana-enterprise.git" - - cd grafana-enterprise - - git checkout ${DRONE_BRANCH} - environment: - GITHUB_TOKEN: - from_secret: github_token - image: grafana/build-container:1.7.5 - name: clone-enterprise -- commands: - - mv bin/grabpl /tmp/ - - rmdir bin - - mv grafana-enterprise /tmp/ - - /tmp/grabpl init-enterprise --github-token $${GITHUB_TOKEN} /tmp/grafana-enterprise - - mv /tmp/grafana-enterprise/deployment_tools_config.json deployment_tools_config.json - - mkdir bin - - mv /tmp/grabpl bin/ - depends_on: - - clone-enterprise - - grabpl - environment: - GITHUB_TOKEN: - from_secret: github_token - image: grafana/build-container:1.7.5 - name: init-enterprise -- commands: - - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd - depends_on: - - init-enterprise - environment: - CGO_ENABLED: 0 - image: golang:1.20.6 - name: compile-build-cmd -- commands: - - make gen-go - depends_on: - - init-enterprise - image: grafana/build-container:1.7.5 - name: wire-install -- commands: - - yarn install --immutable - depends_on: - - init-enterprise - image: grafana/build-container:1.7.5 - name: yarn-install -- commands: - - '# It is required that code generated from Thema/CUE be committed and in sync - with its inputs.' - - '# The following command will fail if running code generators produces any diff - in output.' - - CODEGEN_VERIFY=1 make gen-cue - depends_on: - - init-enterprise - image: grafana/build-container:1.7.5 - name: verify-gen-cue -- commands: - - '# It is required that generated jsonnet is committed and in sync with its inputs.' - - '# The following command will fail if running code generators produces any diff - in output.' - - CODEGEN_VERIFY=1 make gen-jsonnet - depends_on: - - init-enterprise - image: grafana/build-container:1.7.5 - name: verify-gen-jsonnet -- commands: - - ./bin/build build-backend --jobs 8 --edition enterprise --build-id ${DRONE_BUILD_NUMBER} - depends_on: - - wire-install - - compile-build-cmd - image: grafana/build-container:1.7.5 - name: build-backend -- commands: - - ./bin/build build-frontend --jobs 8 --edition enterprise --build-id ${DRONE_BUILD_NUMBER} - depends_on: - - compile-build-cmd - - yarn-install - environment: - NODE_OPTIONS: --max_old_space_size=8192 - image: grafana/build-container:1.7.5 - name: build-frontend -- commands: - - ./bin/build build-frontend-packages --jobs 8 --edition enterprise --build-id ${DRONE_BUILD_NUMBER} - - yarn packages:pack - - ./scripts/validate-npm-packages.sh - depends_on: - - compile-build-cmd - - yarn-install - environment: - NODE_OPTIONS: --max_old_space_size=8192 - image: grafana/build-container:1.7.5 - name: build-frontend-packages -- commands: - - ./bin/build build-plugins --jobs 8 --edition enterprise - depends_on: - - compile-build-cmd - - yarn-install - environment: - GRAFANA_API_KEY: - from_secret: grafana_api_key - image: grafana/build-container:1.7.5 - name: build-plugins -- commands: - - ./bin/build package --jobs 8 --edition enterprise --build-id ${DRONE_BUILD_NUMBER} - --sign - depends_on: - - build-plugins - - build-backend - - build-frontend - - build-frontend-packages - environment: - GPG_KEY_PASSWORD: - from_secret: packages_gpg_passphrase - GPG_PRIV_KEY: - from_secret: packages_gpg_private_key - GPG_PUB_KEY: - from_secret: packages_gpg_public_key - GRAFANA_API_KEY: - from_secret: grafana_api_key - image: grafana/build-container:1.7.5 - name: package -- commands: - - ls dist/*.tar.gz* - - cp dist/*.tar.gz* packaging/docker/ - depends_on: - - package - image: grafana/build-container:1.7.5 - name: copy-packages-for-docker -- commands: - - ./bin/build build-docker --edition enterprise --shouldSave - depends_on: - - copy-packages-for-docker - - compile-build-cmd - environment: - GCP_KEY: - from_secret: gcp_key - image: google/cloud-sdk:431.0.0 - name: build-docker-images - volumes: - - name: docker - path: /var/run/docker.sock -- commands: - - ./bin/build build-docker --edition enterprise --shouldSave --ubuntu - depends_on: - - copy-packages-for-docker - - compile-build-cmd - environment: - GCP_KEY: - from_secret: gcp_key - image: google/cloud-sdk:431.0.0 - name: build-docker-images-ubuntu - volumes: - - name: docker - path: /var/run/docker.sock -- commands: - - ./scripts/grafana-server/start-server - depends_on: - - build-plugins - - build-backend - - build-frontend - - build-frontend-packages - detach: true - environment: - ARCH: linux-amd64 - PORT: 3001 - RUNDIR: scripts/grafana-server/tmp-grafana-enterprise - image: grafana/build-container:1.7.5 - name: grafana-server -- commands: - - apt-get install -y netcat - - ./bin/build e2e-tests --port 3001 --suite dashboards-suite --tries 3 - depends_on: - - grafana-server - environment: - HOST: grafana-server - image: cypress/included:9.5.1-node16.14.0-slim-chrome99-ff97 - name: end-to-end-tests-dashboards-suite -- commands: - - apt-get install -y netcat - - ./bin/build e2e-tests --port 3001 --suite smoke-tests-suite --tries 3 - depends_on: - - grafana-server - environment: - HOST: grafana-server - image: cypress/included:9.5.1-node16.14.0-slim-chrome99-ff97 - name: end-to-end-tests-smoke-tests-suite -- commands: - - apt-get install -y netcat - - ./bin/build e2e-tests --port 3001 --suite panels-suite --tries 3 - depends_on: - - grafana-server - environment: - HOST: grafana-server - image: cypress/included:9.5.1-node16.14.0-slim-chrome99-ff97 - name: end-to-end-tests-panels-suite -- commands: - - apt-get install -y netcat - - ./bin/build e2e-tests --port 3001 --suite various-suite --tries 3 - depends_on: - - grafana-server - environment: - HOST: grafana-server - image: cypress/included:9.5.1-node16.14.0-slim-chrome99-ff97 - name: end-to-end-tests-various-suite -- commands: - - apt-get update - - apt-get install -yq zip - - printenv GCP_GRAFANA_UPLOAD_ARTIFACTS_KEY > /tmp/gcpkey_upload_artifacts.json - - gcloud auth activate-service-account --key-file=/tmp/gcpkey_upload_artifacts.json - - find ./e2e -type f -name "*spec.ts.mp4" | zip e2e/videos.zip -@ - - gsutil cp e2e/videos.zip gs://$${E2E_TEST_ARTIFACTS_BUCKET}/${DRONE_BUILD_NUMBER}/artifacts/videos/videos.zip - - export E2E_ARTIFACTS_VIDEO_ZIP=https://storage.googleapis.com/$${E2E_TEST_ARTIFACTS_BUCKET}/${DRONE_BUILD_NUMBER}/artifacts/videos/videos.zip - - 'echo "E2E Test artifacts uploaded to: $${E2E_ARTIFACTS_VIDEO_ZIP}"' - - 'curl -X POST https://api.github.com/repos/${DRONE_REPO}/statuses/${DRONE_COMMIT_SHA} - -H "Authorization: token $${GITHUB_TOKEN}" -d "{\"state\":\"success\",\"target_url\":\"$${E2E_ARTIFACTS_VIDEO_ZIP}\", - \"description\": \"Click on the details to download e2e recording videos\", \"context\": - \"e2e_artifacts\"}"' - depends_on: - - end-to-end-tests-dashboards-suite - - end-to-end-tests-panels-suite - - end-to-end-tests-smoke-tests-suite - - end-to-end-tests-various-suite - environment: - E2E_TEST_ARTIFACTS_BUCKET: releng-pipeline-artifacts-dev - GCP_GRAFANA_UPLOAD_ARTIFACTS_KEY: - from_secret: gcp_upload_artifacts_key - GITHUB_TOKEN: - from_secret: github_token - failure: ignore - image: google/cloud-sdk:431.0.0 - name: e2e-tests-artifacts-upload - when: - status: - - success - - failure -- commands: - - ./bin/build upload-cdn --edition enterprise - depends_on: - - package - environment: - GCP_KEY: - from_secret: gcp_key - PRERELEASE_BUCKET: - from_secret: prerelease_bucket - image: grafana/grafana-ci-deploy:1.3.3 - name: upload-cdn-assets - when: - repo: - - grafana/grafana -- commands: - - ./bin/build upload-packages --edition enterprise - depends_on: - - package - environment: - GCP_KEY: - from_secret: gcp_key - PRERELEASE_BUCKET: - from_secret: prerelease_bucket - image: grafana/grafana-ci-deploy:1.3.3 - name: upload-packages - when: - repo: - - grafana/grafana -trigger: - ref: - - refs/heads/v[0-9]* -type: docker -volumes: -- host: - path: /var/run/docker.sock - name: docker -- name: postgres - temp: - medium: memory -- name: mysql57 - temp: - medium: memory -- name: mysql80 - temp: - medium: memory ---- -clone: - disable: true -depends_on: [] -environment: - EDITION: enterprise -image_pull_secrets: -- dockerconfigjson -kind: pipeline -name: release-branch-enterprise-test-frontend -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/v3.0.39/grabpl - - chmod +x bin/grabpl - image: byrnedo/alpine-curl:0.1.8 - name: grabpl -- commands: - - git clone "https://$${GITHUB_TOKEN}@github.com/grafana/grafana-enterprise.git" - - cd grafana-enterprise - - git checkout ${DRONE_BRANCH} - environment: - GITHUB_TOKEN: - from_secret: github_token - image: grafana/build-container:1.7.5 - name: clone-enterprise -- commands: - - mv bin/grabpl /tmp/ - - rmdir bin - - mv grafana-enterprise /tmp/ - - /tmp/grabpl init-enterprise --github-token $${GITHUB_TOKEN} /tmp/grafana-enterprise - - mv /tmp/grafana-enterprise/deployment_tools_config.json deployment_tools_config.json - - mkdir bin - - mv /tmp/grabpl bin/ - depends_on: - - clone-enterprise - - grabpl - environment: - GITHUB_TOKEN: - from_secret: github_token - image: grafana/build-container:1.7.5 - name: init-enterprise -- commands: - - echo $DRONE_RUNNER_NAME - image: alpine:3.17.1 - name: identify-runner -- commands: - - yarn install --immutable - depends_on: - - init-enterprise - image: grafana/build-container:1.7.5 - name: yarn-install -- commands: - - yarn betterer ci - depends_on: - - init-enterprise - - yarn-install - image: grafana/build-container:1.7.5 - name: betterer-frontend -- commands: - - yarn run ci:test-frontend - depends_on: - - init-enterprise - - yarn-install - environment: - TEST_MAX_WORKERS: 50% - image: grafana/build-container:1.7.5 - name: test-frontend -trigger: - ref: - - refs/heads/v[0-9]* -type: docker -volumes: -- host: - path: /var/run/docker.sock - name: docker ---- -clone: - disable: true -depends_on: [] -environment: - EDITION: enterprise -image_pull_secrets: -- dockerconfigjson -kind: pipeline -name: release-branch-enterprise-test-backend -node: - type: no-parallel -platform: - arch: amd64 - os: linux -services: [] -steps: -- commands: - - git clone "https://$${GITHUB_TOKEN}@github.com/grafana/grafana-enterprise.git" - - cd grafana-enterprise - - git checkout ${DRONE_BRANCH} - environment: - GITHUB_TOKEN: - from_secret: github_token - image: grafana/build-container:1.7.5 - name: clone-enterprise -- commands: - - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.39/grabpl - - chmod +x bin/grabpl - image: byrnedo/alpine-curl:0.1.8 - name: grabpl -- commands: - - mv bin/grabpl /tmp/ - - rmdir bin - - mv grafana-enterprise /tmp/ - - /tmp/grabpl init-enterprise --github-token $${GITHUB_TOKEN} /tmp/grafana-enterprise - - mv /tmp/grafana-enterprise/deployment_tools_config.json deployment_tools_config.json - - mkdir bin - - mv /tmp/grabpl bin/ - depends_on: - - clone-enterprise - - grabpl - environment: - GITHUB_TOKEN: - from_secret: github_token - image: grafana/build-container:1.7.5 - name: init-enterprise -- commands: - - echo $DRONE_RUNNER_NAME - image: alpine:3.17.1 - name: identify-runner -- commands: - - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd - depends_on: - - init-enterprise - environment: - CGO_ENABLED: 0 - image: golang:1.20.6 - name: compile-build-cmd -- commands: - - '# It is required that code generated from Thema/CUE be committed and in sync - with its inputs.' - - '# The following command will fail if running code generators produces any diff - in output.' - - CODEGEN_VERIFY=1 make gen-cue - depends_on: - - init-enterprise - image: grafana/build-container:1.7.5 - name: verify-gen-cue -- commands: - - '# It is required that generated jsonnet is committed and in sync with its inputs.' - - '# The following command will fail if running code generators produces any diff - in output.' - - CODEGEN_VERIFY=1 make gen-jsonnet - depends_on: - - init-enterprise - image: grafana/build-container:1.7.5 - name: verify-gen-jsonnet -- commands: - - make gen-go - depends_on: - - verify-gen-cue - image: grafana/build-container:1.7.5 - name: wire-install -- commands: - - go test -tags requires_buildifer -short -covermode=atomic -timeout=5m ./pkg/... - depends_on: - - wire-install - image: grafana/build-container:1.7.5 - name: test-backend -- commands: - - go test -count=1 -covermode=atomic -timeout=5m -run '^TestIntegration' $(find - ./pkg -type f -name '*_test.go' -exec grep -l '^func TestIntegration' '{}' '+' - | grep -o '\(.*\)/' | sort -u) - depends_on: - - wire-install - image: grafana/build-container:1.7.5 - name: test-backend-integration -trigger: - ref: - - refs/heads/v[0-9]* -type: docker -volumes: -- host: - path: /var/run/docker.sock - name: docker ---- -clone: - disable: true -depends_on: [] -environment: - EDITION: enterprise -image_pull_secrets: -- dockerconfigjson -kind: pipeline -name: release-branch-enterprise-integration-tests -node: - type: no-parallel -platform: - arch: amd64 - os: linux -services: -- environment: - PGDATA: /var/lib/postgresql/data/pgdata - POSTGRES_DB: grafanatest - POSTGRES_PASSWORD: grafanatest - POSTGRES_USER: grafanatest - image: postgres:12.3-alpine - name: postgres - volumes: - - name: postgres - path: /var/lib/postgresql/data/pgdata -- commands: - - docker-entrypoint.sh mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci - environment: - MYSQL_DATABASE: grafana_tests - MYSQL_PASSWORD: password - MYSQL_ROOT_PASSWORD: rootpass - MYSQL_USER: grafana - image: mysql:5.7.39 - name: mysql57 - volumes: - - name: mysql57 - path: /var/lib/mysql -- commands: - - docker-entrypoint.sh mysqld --default-authentication-plugin=mysql_native_password - environment: - MYSQL_DATABASE: grafana_tests - MYSQL_PASSWORD: password - MYSQL_ROOT_PASSWORD: rootpass - MYSQL_USER: grafana - image: mysql:8.0.32 - name: mysql80 - volumes: - - name: mysql80 - path: /var/lib/mysql -- environment: {} - image: redis:6.2.11-alpine - name: redis -- environment: {} - image: memcached:1.6.9-alpine - name: memcached -steps: -- commands: - - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.39/grabpl - - chmod +x bin/grabpl - image: byrnedo/alpine-curl:0.1.8 - name: grabpl -- commands: - - echo $DRONE_RUNNER_NAME - image: alpine:3.17.1 - name: identify-runner -- commands: - - git clone "https://$${GITHUB_TOKEN}@github.com/grafana/grafana-enterprise.git" - - cd grafana-enterprise - - git checkout ${DRONE_BRANCH} - environment: - GITHUB_TOKEN: - from_secret: github_token - image: grafana/build-container:1.7.5 - name: clone-enterprise -- commands: - - mv bin/grabpl /tmp/ - - rmdir bin - - mv grafana-enterprise /tmp/ - - /tmp/grabpl init-enterprise --github-token $${GITHUB_TOKEN} /tmp/grafana-enterprise - - mv /tmp/grafana-enterprise/deployment_tools_config.json deployment_tools_config.json - - mkdir bin - - mv /tmp/grabpl bin/ - depends_on: - - clone-enterprise - - grabpl - environment: - GITHUB_TOKEN: - from_secret: github_token - image: grafana/build-container:1.7.5 - name: init-enterprise -- commands: - - '# It is required that code generated from Thema/CUE be committed and in sync - with its inputs.' - - '# The following command will fail if running code generators produces any diff - in output.' - - CODEGEN_VERIFY=1 make gen-cue - depends_on: - - init-enterprise - image: grafana/build-container:1.7.5 - name: verify-gen-cue -- commands: - - '# It is required that generated jsonnet is committed and in sync with its inputs.' - - '# The following command will fail if running code generators produces any diff - in output.' - - CODEGEN_VERIFY=1 make gen-jsonnet - depends_on: - - init-enterprise - image: grafana/build-container:1.7.5 - name: verify-gen-jsonnet -- commands: - - make gen-go - depends_on: - - verify-gen-cue - image: grafana/build-container:1.7.5 - name: wire-install -- commands: - - apt-get update - - apt-get install -yq postgresql-client - - dockerize -wait tcp://postgres:5432 -timeout 120s - - psql -p 5432 -h postgres -U grafanatest -d grafanatest -f devenv/docker/blocks/postgres_tests/setup.sql - - go clean -testcache - - go test -p=1 -count=1 -covermode=atomic -timeout=5m -run '^TestIntegration' $(find - ./pkg -type f -name '*_test.go' -exec grep -l '^func TestIntegration' '{}' '+' - | grep -o '\(.*\)/' | sort -u) - depends_on: - - wire-install - environment: - GRAFANA_TEST_DB: postgres - PGPASSWORD: grafanatest - POSTGRES_HOST: postgres - image: grafana/build-container:1.7.5 - name: postgres-integration-tests -- commands: - - apt-get update - - apt-get install -yq default-mysql-client - - dockerize -wait tcp://mysql57:3306 -timeout 120s - - cat devenv/docker/blocks/mysql_tests/setup.sql | mysql -h mysql57 -P 3306 -u root - -prootpass - - go clean -testcache - - go test -p=1 -count=1 -covermode=atomic -timeout=5m -run '^TestIntegration' $(find - ./pkg -type f -name '*_test.go' -exec grep -l '^func TestIntegration' '{}' '+' - | grep -o '\(.*\)/' | sort -u) - depends_on: - - wire-install - environment: - GRAFANA_TEST_DB: mysql - MYSQL_HOST: mysql57 - image: grafana/build-container:1.7.5 - name: mysql-5.7-integration-tests -- commands: - - apt-get update - - apt-get install -yq default-mysql-client - - dockerize -wait tcp://mysql80:3306 -timeout 120s - - cat devenv/docker/blocks/mysql_tests/setup.sql | mysql -h mysql80 -P 3306 -u root - -prootpass - - go clean -testcache - - go test -p=1 -count=1 -covermode=atomic -timeout=5m -run '^TestIntegration' $(find - ./pkg -type f -name '*_test.go' -exec grep -l '^func TestIntegration' '{}' '+' - | grep -o '\(.*\)/' | sort -u) - depends_on: - - wire-install - environment: - GRAFANA_TEST_DB: mysql - MYSQL_HOST: mysql80 - image: grafana/build-container:1.7.5 - name: mysql-8.0-integration-tests -- commands: - - dockerize -wait tcp://redis:6379/0 -timeout 120s - - go clean -testcache - - go test -run IntegrationRedis -covermode=atomic -timeout=2m ./pkg/... - depends_on: - - wire-install - environment: - REDIS_URL: redis://redis:6379/0 - image: grafana/build-container:1.7.5 - name: redis-integration-tests -- commands: - - dockerize -wait tcp://memcached:11211 -timeout 120s - - go clean -testcache - - go test -run IntegrationMemcached -covermode=atomic -timeout=2m ./pkg/... - depends_on: - - wire-install - environment: - MEMCACHED_HOSTS: memcached:11211 - image: grafana/build-container:1.7.5 - name: memcached-integration-tests -trigger: - ref: - - refs/heads/v[0-9]* -type: docker -volumes: -- host: - path: /var/run/docker.sock - name: docker -- name: postgres - temp: - medium: memory -- name: mysql57 - temp: - medium: memory -- name: mysql80 - temp: - medium: memory ---- -clone: - disable: true -depends_on: -- release-branch-enterprise-build-e2e-publish -- release-branch-enterprise-test-frontend -- release-branch-enterprise-test-backend -environment: - EDITION: enterprise -image_pull_secrets: -- dockerconfigjson -kind: pipeline -name: release-branch-enterprise-windows -platform: - arch: amd64 - os: windows - version: "1809" -services: [] -steps: -- commands: - - echo $env:DRONE_RUNNER_NAME - image: mcr.microsoft.com/windows:1809 - name: identify-runner -- commands: - - $$ProgressPreference = "SilentlyContinue" - - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.39/windows/grabpl.exe - -OutFile grabpl.exe - - git clone "https://$$env:GITHUB_TOKEN@github.com/grafana/grafana-enterprise.git" - - cd grafana-enterprise - - git checkout $$env:DRONE_BRANCH - environment: - GITHUB_TOKEN: - from_secret: github_token - image: grafana/ci-wix:0.1.1 - name: clone -- commands: - - cp -r grafana-enterprise C:\App\grafana-enterprise - - rm -r -force grafana-enterprise - - cp grabpl.exe C:\App\grabpl.exe - - rm -force grabpl.exe - - C:\App\grabpl.exe init-enterprise --github-token $$env:GITHUB_TOKEN C:\App\grafana-enterprise - $$env:DRONE_BRANCH - - cp C:\App\grabpl.exe grabpl.exe - depends_on: - - clone - environment: - GITHUB_TOKEN: - from_secret: github_token - image: grafana/ci-wix:0.1.1 - name: windows-init -- commands: - - $$gcpKey = $$env:GCP_KEY - - '[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($$gcpKey)) - > gcpkey.json' - - dos2unix gcpkey.json - - gcloud auth activate-service-account --key-file=gcpkey.json - - rm gcpkey.json - - cp C:\App\nssm-2.24.zip . - depends_on: - - windows-init - environment: - GCP_KEY: - from_secret: gcp_key - GITHUB_TOKEN: - from_secret: github_token - PRERELEASE_BUCKET: - from_secret: prerelease_bucket - image: grafana/ci-wix:0.1.1 - name: build-windows-installer -trigger: - ref: - - refs/heads/v[0-9]* -type: docker -volumes: -- host: - path: //./pipe/docker_engine/ - name: docker ---- -clone: - disable: true -depends_on: [] -environment: - EDITION: enterprise2 -image_pull_secrets: -- dockerconfigjson -kind: pipeline -name: release-branch-enterprise2-build-e2e-publish -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/v3.0.39/grabpl - - chmod +x bin/grabpl - image: byrnedo/alpine-curl:0.1.8 - name: grabpl -- commands: - - echo $DRONE_RUNNER_NAME - image: alpine:3.17.1 - name: identify-runner -- commands: - - git clone "https://$${GITHUB_TOKEN}@github.com/grafana/grafana-enterprise.git" - - cd grafana-enterprise - - git checkout ${DRONE_BRANCH} - environment: - GITHUB_TOKEN: - from_secret: github_token - image: grafana/build-container:1.7.5 - name: clone-enterprise -- commands: - - mv bin/grabpl /tmp/ - - rmdir bin - - mv grafana-enterprise /tmp/ - - /tmp/grabpl init-enterprise --github-token $${GITHUB_TOKEN} /tmp/grafana-enterprise - - mv /tmp/grafana-enterprise/deployment_tools_config.json deployment_tools_config.json - - mkdir bin - - mv /tmp/grabpl bin/ - depends_on: - - clone-enterprise - - grabpl - environment: - GITHUB_TOKEN: - from_secret: github_token - image: grafana/build-container:1.7.5 - name: init-enterprise -- commands: - - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd - depends_on: - - init-enterprise - environment: - CGO_ENABLED: 0 - image: golang:1.20.6 - name: compile-build-cmd -- commands: - - make gen-go - depends_on: - - init-enterprise - image: grafana/build-container:1.7.5 - name: wire-install -- commands: - - yarn install --immutable - depends_on: - - init-enterprise - image: grafana/build-container:1.7.5 - name: yarn-install -- commands: - - '# It is required that code generated from Thema/CUE be committed and in sync - with its inputs.' - - '# The following command will fail if running code generators produces any diff - in output.' - - CODEGEN_VERIFY=1 make gen-cue - depends_on: - - init-enterprise - image: grafana/build-container:1.7.5 - name: verify-gen-cue -- commands: - - ./bin/build build-frontend --jobs 8 --edition enterprise --build-id ${DRONE_BUILD_NUMBER} - depends_on: - - compile-build-cmd - - yarn-install - environment: - NODE_OPTIONS: --max_old_space_size=8192 - image: grafana/build-container:1.7.5 - name: build-frontend -- commands: - - ./bin/build build-frontend-packages --jobs 8 --edition enterprise --build-id ${DRONE_BUILD_NUMBER} - - yarn packages:pack - - ./scripts/validate-npm-packages.sh - depends_on: - - compile-build-cmd - - yarn-install - environment: - NODE_OPTIONS: --max_old_space_size=8192 - image: grafana/build-container:1.7.5 - name: build-frontend-packages -- commands: - - ./bin/build build-plugins --jobs 8 --edition enterprise - depends_on: - - compile-build-cmd - - yarn-install - environment: - GRAFANA_API_KEY: - from_secret: grafana_api_key - image: grafana/build-container:1.7.5 - name: build-plugins -- commands: - - ./bin/build build-backend --jobs 8 --edition enterprise2 --build-id ${DRONE_BUILD_NUMBER} - --variants linux-amd64 - depends_on: - - wire-install - - compile-build-cmd - image: grafana/build-container:1.7.5 - name: build-backend-enterprise2 -- commands: - - ./bin/build package --jobs 8 --edition enterprise2 --build-id ${DRONE_BUILD_NUMBER} - --sign - depends_on: - - build-plugins - - build-backend-enterprise2 - - build-frontend - - build-frontend-packages - environment: - GPG_KEY_PASSWORD: - from_secret: packages_gpg_passphrase - GPG_PRIV_KEY: - from_secret: packages_gpg_private_key - GPG_PUB_KEY: - from_secret: packages_gpg_public_key - GRAFANA_API_KEY: - from_secret: grafana_api_key - image: grafana/build-container:1.7.5 - name: package-enterprise2 -- commands: - - ./bin/build upload-cdn --edition enterprise2 - depends_on: - - package-enterprise2 - environment: - ENTERPRISE2_CDN_PATH: - from_secret: enterprise2-cdn-path - GCP_KEY: - from_secret: gcp_key - PRERELEASE_BUCKET: - from_secret: prerelease_bucket - image: grafana/grafana-ci-deploy:1.3.3 - name: upload-cdn-assets-enterprise2 -- commands: - - ls dist/*.tar.gz* - - cp dist/*.tar.gz* packaging/docker/ - depends_on: - - package-enterprise2 - image: grafana/build-container:1.7.5 - name: copy-packages-for-docker -- commands: - - ./bin/build build-docker --edition enterprise2 --shouldSave - depends_on: - - copy-packages-for-docker - - compile-build-cmd - environment: - DOCKER_ENTERPRISE2_REPO: - from_secret: docker_enterprise2_repo - GCP_KEY: - from_secret: gcp_key - image: google/cloud-sdk:431.0.0 - name: build-docker-images - volumes: - - name: docker - path: /var/run/docker.sock -- commands: - - ./bin/build build-docker --edition enterprise2 --shouldSave --ubuntu - depends_on: - - copy-packages-for-docker - - compile-build-cmd - environment: - DOCKER_ENTERPRISE2_REPO: - from_secret: docker_enterprise2_repo - GCP_KEY: - from_secret: gcp_key - image: google/cloud-sdk:431.0.0 - name: build-docker-images-ubuntu - volumes: - - name: docker - path: /var/run/docker.sock -- commands: - - ./bin/build artifacts docker fetch --edition enterprise2 - depends_on: - - build-docker-images - - build-docker-images-ubuntu - environment: - DOCKER_ENTERPRISE2_REPO: - from_secret: docker_enterprise2_repo - DOCKER_PASSWORD: - from_secret: docker_password - DOCKER_USER: - from_secret: docker_username - GCP_KEY: - from_secret: gcp_key - image: google/cloud-sdk:431.0.0 - name: fetch-images-enterprise2 - volumes: - - name: docker - path: /var/run/docker.sock -- commands: - - ./bin/build artifacts docker publish-enterprise2 --dockerhub-repo $${DOCKER_ENTERPRISE2_REPO} - depends_on: - - fetch-images-enterprise2 - environment: - DOCKER_ENTERPRISE2_REPO: - from_secret: docker_enterprise2_repo - DOCKER_PASSWORD: - from_secret: docker_password - DOCKER_USER: - from_secret: docker_username - GCP_KEY: - from_secret: gcp_key_hg - GITHUB_APP_ID: - from_secret: delivery-bot-app-id - GITHUB_APP_INSTALLATION_ID: - from_secret: delivery-bot-app-installation-id - GITHUB_APP_PRIVATE_KEY: - from_secret: delivery-bot-app-private-key - image: google/cloud-sdk:431.0.0 - name: publish-images-enterprise2 - volumes: - - name: docker - path: /var/run/docker.sock -- commands: - - ./bin/build upload-packages --edition enterprise2 - depends_on: - - package-enterprise2 - environment: - GCP_KEY: - from_secret: gcp_key - PRERELEASE_BUCKET: - from_secret: prerelease_bucket - image: grafana/grafana-ci-deploy:1.3.3 - name: upload-packages-enterprise2 -trigger: - ref: - - refs/heads/v[0-9]* -type: docker -volumes: -- host: - path: /var/run/docker.sock - name: docker -- name: postgres - temp: - medium: memory -- name: mysql57 - temp: - medium: memory -- name: mysql80 - temp: - medium: memory ---- clone: retries: 3 depends_on: [] @@ -6552,7 +4208,7 @@ environment: image_pull_secrets: - dockerconfigjson kind: pipeline -name: integration-tests-oss +name: integration-tests node: type: no-parallel platform: @@ -6726,213 +4382,6 @@ volumes: clone: disable: true depends_on: [] -environment: - EDITION: enterprise -image_pull_secrets: -- dockerconfigjson -kind: pipeline -name: integration-tests-enterprise -node: - type: no-parallel -platform: - arch: amd64 - os: linux -services: -- environment: - PGDATA: /var/lib/postgresql/data/pgdata - POSTGRES_DB: grafanatest - POSTGRES_PASSWORD: grafanatest - POSTGRES_USER: grafanatest - image: postgres:12.3-alpine - name: postgres - volumes: - - name: postgres - path: /var/lib/postgresql/data/pgdata -- commands: - - docker-entrypoint.sh mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci - environment: - MYSQL_DATABASE: grafana_tests - MYSQL_PASSWORD: password - MYSQL_ROOT_PASSWORD: rootpass - MYSQL_USER: grafana - image: mysql:5.7.39 - name: mysql57 - volumes: - - name: mysql57 - path: /var/lib/mysql -- commands: - - docker-entrypoint.sh mysqld --default-authentication-plugin=mysql_native_password - environment: - MYSQL_DATABASE: grafana_tests - MYSQL_PASSWORD: password - MYSQL_ROOT_PASSWORD: rootpass - MYSQL_USER: grafana - image: mysql:8.0.32 - name: mysql80 - volumes: - - name: mysql80 - path: /var/lib/mysql -- environment: {} - image: redis:6.2.11-alpine - name: redis -- environment: {} - image: memcached:1.6.9-alpine - name: memcached -steps: -- commands: - - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.39/grabpl - - chmod +x bin/grabpl - image: byrnedo/alpine-curl:0.1.8 - name: grabpl -- commands: - - echo $DRONE_RUNNER_NAME - image: alpine:3.17.1 - name: identify-runner -- commands: - - git clone "https://$${GITHUB_TOKEN}@github.com/grafana/grafana-enterprise.git" - - cd grafana-enterprise - - git checkout ${DRONE_TAG} - environment: - GITHUB_TOKEN: - from_secret: github_token - image: grafana/build-container:1.7.5 - name: clone-enterprise -- commands: - - mv bin/grabpl /tmp/ - - rmdir bin - - mv grafana-enterprise /tmp/ - - /tmp/grabpl init-enterprise --github-token $${GITHUB_TOKEN} /tmp/grafana-enterprise - ${DRONE_TAG} - - mv /tmp/grafana-enterprise/deployment_tools_config.json deployment_tools_config.json - - mkdir bin - - mv /tmp/grabpl bin/ - depends_on: - - clone-enterprise - - grabpl - environment: - GITHUB_TOKEN: - from_secret: github_token - image: grafana/build-container:1.7.5 - name: init-enterprise -- commands: - - '# It is required that code generated from Thema/CUE be committed and in sync - with its inputs.' - - '# The following command will fail if running code generators produces any diff - in output.' - - CODEGEN_VERIFY=1 make gen-cue - depends_on: - - init-enterprise - image: grafana/build-container:1.7.5 - name: verify-gen-cue -- commands: - - '# It is required that generated jsonnet is committed and in sync with its inputs.' - - '# The following command will fail if running code generators produces any diff - in output.' - - CODEGEN_VERIFY=1 make gen-jsonnet - depends_on: - - init-enterprise - image: grafana/build-container:1.7.5 - name: verify-gen-jsonnet -- commands: - - make gen-go - depends_on: - - verify-gen-cue - image: grafana/build-container:1.7.5 - name: wire-install -- commands: - - apt-get update - - apt-get install -yq postgresql-client - - dockerize -wait tcp://postgres:5432 -timeout 120s - - psql -p 5432 -h postgres -U grafanatest -d grafanatest -f devenv/docker/blocks/postgres_tests/setup.sql - - go clean -testcache - - go test -p=1 -count=1 -covermode=atomic -timeout=5m -run '^TestIntegration' $(find - ./pkg -type f -name '*_test.go' -exec grep -l '^func TestIntegration' '{}' '+' - | grep -o '\(.*\)/' | sort -u) - depends_on: - - wire-install - environment: - GRAFANA_TEST_DB: postgres - PGPASSWORD: grafanatest - POSTGRES_HOST: postgres - image: grafana/build-container:1.7.5 - name: postgres-integration-tests -- commands: - - apt-get update - - apt-get install -yq default-mysql-client - - dockerize -wait tcp://mysql57:3306 -timeout 120s - - cat devenv/docker/blocks/mysql_tests/setup.sql | mysql -h mysql57 -P 3306 -u root - -prootpass - - go clean -testcache - - go test -p=1 -count=1 -covermode=atomic -timeout=5m -run '^TestIntegration' $(find - ./pkg -type f -name '*_test.go' -exec grep -l '^func TestIntegration' '{}' '+' - | grep -o '\(.*\)/' | sort -u) - depends_on: - - wire-install - environment: - GRAFANA_TEST_DB: mysql - MYSQL_HOST: mysql57 - image: grafana/build-container:1.7.5 - name: mysql-5.7-integration-tests -- commands: - - apt-get update - - apt-get install -yq default-mysql-client - - dockerize -wait tcp://mysql80:3306 -timeout 120s - - cat devenv/docker/blocks/mysql_tests/setup.sql | mysql -h mysql80 -P 3306 -u root - -prootpass - - go clean -testcache - - go test -p=1 -count=1 -covermode=atomic -timeout=5m -run '^TestIntegration' $(find - ./pkg -type f -name '*_test.go' -exec grep -l '^func TestIntegration' '{}' '+' - | grep -o '\(.*\)/' | sort -u) - depends_on: - - wire-install - environment: - GRAFANA_TEST_DB: mysql - MYSQL_HOST: mysql80 - image: grafana/build-container:1.7.5 - name: mysql-8.0-integration-tests -- commands: - - dockerize -wait tcp://redis:6379/0 -timeout 120s - - go clean -testcache - - go test -run IntegrationRedis -covermode=atomic -timeout=2m ./pkg/... - depends_on: - - wire-install - environment: - REDIS_URL: redis://redis:6379/0 - image: grafana/build-container:1.7.5 - name: redis-integration-tests -- commands: - - dockerize -wait tcp://memcached:11211 -timeout 120s - - go clean -testcache - - go test -run IntegrationMemcached -covermode=atomic -timeout=2m ./pkg/... - depends_on: - - wire-install - environment: - MEMCACHED_HOSTS: memcached:11211 - image: grafana/build-container:1.7.5 - name: memcached-integration-tests -trigger: - event: - - promote - target: integration-tests -type: docker -volumes: -- host: - path: /var/run/docker.sock - name: docker -- name: postgres - temp: - medium: memory -- name: mysql57 - temp: - medium: memory -- name: mysql80 - temp: - medium: memory ---- -clone: - disable: true -depends_on: [] image_pull_secrets: - dockerconfigjson kind: pipeline @@ -7340,24 +4789,6 @@ get: kind: secret name: packages_secret_access_key --- -get: - name: aws_region - path: secret/data/common/aws-marketplace -kind: secret -name: aws_region ---- -get: - name: aws_access_key_id - path: secret/data/common/aws-marketplace -kind: secret -name: aws_access_key_id ---- -get: - name: aws_secret_access_key - path: secret/data/common/aws-marketplace -kind: secret -name: aws_secret_access_key ---- get: name: static_asset_editions path: infra/data/ci/grafana-release-eng/artifact-publishing @@ -7413,6 +4844,6 @@ kind: secret name: delivery-bot-app-private-key --- kind: signature -hmac: 0d2d6275d62dea9c46f950ecd6a94c8310f760b2b47347a106f122578d99812d +hmac: 6a12c2bfaa40e37115604628eb130aecd22036ecbb54c1591c584051ce2ca349 ... diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index aeb3b1e2595..1e527dc4156 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -33,23 +33,25 @@ /devenv/README.md @grafana/docs-grafana # Technical documentation -/docs/ @Eve832 @jdbaldry -/docs/sources/ @Eve832 -/docs/sources/administration/ @Eve832 @GrafanaWriter -/docs/sources/alerting/ @brendamuir -/docs/sources/dashboards/ @imatwawana -/docs/sources/datasources/ @lwandz13 -/docs/sources/explore/ @Eve832 @GrafanaWriter -/docs/sources/fundamentals @chri2547 -/docs/sources/getting-started/ @chri2547 -/docs/sources/introduction/ @chri2547 -/docs/sources/old-alerting/ @brendamuir -/docs/sources/panels-visualizations/ @imatwawana -/docs/sources/release-notes/ @Eve832 @GrafanaWriter -/docs/sources/setup-grafana/ @chri2547 -/docs/sources/upgrade-guide/ @chri2547 @imatwawana -/docs/sources/whatsnew/ @chri2547 @imatwawana -/docs/sources/developers/plugins/ @Eve832 @josmperez @grafana/plugins-platform-frontend @grafana/plugins-platform-backend +# `make docs` procedure and related workflows are owned @grafana/docs-tooling. Slack #docs. +# Documentation sources might have different owners. +/docs/ @grafana/docs-tooling +/docs/sources/ @Eve832 +/docs/sources/administration/ @Eve832 @GrafanaWriter +/docs/sources/alerting/ @brendamuir +/docs/sources/dashboards/ @imatwawana +/docs/sources/datasources/ @lwandz13 +/docs/sources/explore/ @Eve832 @GrafanaWriter +/docs/sources/fundamentals @chri2547 +/docs/sources/getting-started/ @chri2547 +/docs/sources/introduction/ @chri2547 +/docs/sources/old-alerting/ @brendamuir +/docs/sources/panels-visualizations/ @imatwawana +/docs/sources/release-notes/ @Eve832 @GrafanaWriter +/docs/sources/setup-grafana/ @chri2547 +/docs/sources/upgrade-guide/ @chri2547 @imatwawana +/docs/sources/whatsnew/ @chri2547 @imatwawana +/docs/sources/developers/plugins/ @Eve832 @josmperez @grafana/plugins-platform-frontend @grafana/plugins-platform-backend /docs/sources/developers/plugins/introduction-to-plugin-development/backend/ @Eve832 @grafana/plugins-platform-backend # Backend code @@ -596,7 +598,7 @@ embed.go @grafana/grafana-as-code /.github/workflows/codeql-analysis.yml @DanCech /.github/workflows/commands.yml @torkelo /.github/workflows/detect-breaking-changes-* @grafana/plugins-platform-frontend -/.github/workflows/doc-validator.yml @grafana/docs-grafana +/.github/workflows/doc-validator.yml @grafana/docs-tooling /.github/workflows/epic-add-to-platform-ux-parent-project.yml @meanmina /.github/workflows/github-release.yml @torkelo /.github/workflows/issue-labeled.yml @armandgrillet @@ -611,21 +613,23 @@ embed.go @grafana/grafana-as-code /.github/workflows/pr-commands-closed.yml @tolzhabayev /.github/workflows/pr-commands.yml @marefr /.github/workflows/pr-patch-check.yml @grafana/grafana-delivery -/.github/workflows/pr-patch-mirror-and-apply.yml @grafana/grafana-delivery -/.github/workflows/publish-technical-documentation-next.yml @grafana/docs-grafana -/.github/workflows/publish-technical-documentation-release.yml @grafana/docs-grafana +/.github/workflows/sync-mirror.yml @grafana/grafana-delivery +/.github/workflows/publish-technical-documentation-next.yml @grafana/docs-tooling +/.github/workflows/publish-technical-documentation-release.yml @grafana/docs-tooling /.github/workflows/remove-milestone.yml @grafana/grafana-delivery /.github/workflows/sbom-report.yml @grafana/security-team /.github/workflows/scripts/json-file-to-job-output.js @grafana/plugins-platform-frontend /.github/workflows/scripts/pr-get-job-link.js @grafana/plugins-platform-frontend /.github/workflows/stale.yml @grafana/grafana-delivery /.github/workflows/update-changelog.yml @grafana/grafana-delivery +/.github/workflows/update-make-docs.yml @grafana/docs-tooling /.github/workflows/snyk.yml @grafana/security-team /.github/workflows/scripts/kinds/verify-kinds.go @grafana/grafana-as-code /.github/workflows/publish-kinds-next.yml @grafana/grafana-as-code /.github/workflows/publish-kinds-release.yml @grafana/grafana-as-code /.github/workflows/verify-kinds.yml @grafana/grafana-as-code /.github/workflows/dashboards-issue-add-label.yml @grafana/dashboards-squad +/.github/workflows/ephemeral-instances.yml @grafana/grafana-operator-experience-squad # Generated files not requiring owner approval diff --git a/.github/workflows/doc-validator.yml b/.github/workflows/doc-validator.yml index 792506ff46d..88a731676fc 100644 --- a/.github/workflows/doc-validator.yml +++ b/.github/workflows/doc-validator.yml @@ -7,7 +7,7 @@ jobs: doc-validator: runs-on: "ubuntu-latest" container: - image: "grafana/doc-validator:v3.0.0" + image: "grafana/doc-validator:v3.2.0" steps: - name: "Checkout code" uses: "actions/checkout@v3" diff --git a/.github/workflows/ephemeral-instances.yml b/.github/workflows/ephemeral-instances.yml new file mode 100644 index 00000000000..2e2dcb04775 --- /dev/null +++ b/.github/workflows/ephemeral-instances.yml @@ -0,0 +1,81 @@ +name: 'Ephemeral instances action' +on: + issue_comment: + types: [created] + pull_request: + types: [opened, reopened, closed] +jobs: + handle-pull-request-event: + runs-on: ubuntu-latest + continue-on-error: true + steps: + - name: Get pull request repo + if: github.triggering_actor == 'PoorlyDefinedBehaviour' + uses: actions/github-script@v6 + id: get-pull-request-repo + with: + result-encoding: string + script: | + if(context.payload.pull_request?.head.ref) { + return context.payload.pull_request.head.ref + } + + const response = await github.rest.pulls.get({ + owner: "grafana", + repo: "grafana", + pull_number: context.payload.issue.number, + }); + + return response.data.head.ref + + - name: Checkout Grafana + if: github.triggering_actor == 'PoorlyDefinedBehaviour' + uses: actions/checkout@v3 + with: + repository: grafana/grafana + ref: ${{ steps.get-pull-request-repo.outputs.result }} + path: grafana + + - name: Setup Go + if: github.triggering_actor == 'PoorlyDefinedBehaviour' + uses: actions/setup-go@v4 + with: + go-version: '>=1.20' + + - name: Generate a GitHub app installation token + if: github.triggering_actor == 'PoorlyDefinedBehaviour' + id: generate_token + uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 + with: + app_id: ${{ secrets.EI_APP_ID }} + private_key: ${{ secrets.EI_APP_PRIVATE_KEY }} + + - name: Checkout ephemeral instances repository + uses: actions/checkout@v3 + with: + repository: grafana/ephemeral-grafana-instances-github-action + token: ${{ steps.generate_token.outputs.token }} + ref: main + path: ephemeral + + - name: Run action + if: github.triggering_actor == 'PoorlyDefinedBehaviour' + env: + GITHUB_EVENT: ${{ toJson(github.event)}} + run: | + GRAFANA_VERSION=10.1.0 + + cd $GITHUB_WORKSPACE/ephemeral/src + go run . \ + -GITHUB_TOKEN="${{ steps.generate_token.outputs.token }}" \ + -GITHUB_EVENT="$GITHUB_EVENT" \ + -GITHUB_TRIGGERING_ACTOR="${{ github.triggering_actor }}" \ + -GCOM_HOST="${{ secrets.EI_GCOM_HOST }}" \ + -GCOM_TOKEN="${{ secrets.EI_GCOM_TOKEN }}" \ + -HOSTED_GRAFANA_IMAGE_TAG="$GRAFANA_VERSION-ephemeral-grafana-${{ github.triggering_actor }}-${{ github.run_number }}-${{ github.run_attempt }}" \ + -REGISTRY="${{ secrets.EI_EPHEMERAL_INSTANCES_REGISTRY }}" \ + -GRAFANA_DIR="$GITHUB_WORKSPACE/grafana" \ + -ENTERPRISE_REF="main" \ + -GRAFANA_VERSION="$GRAFANA_VERSION" \ + -GCP_SERVICE_ACCOUNT_KEY_BASE64="${{ secrets.EI_GCP_SERVICE_ACCOUNT_KEY_BASE64 }}" \ + -EPHEMERAL_ORG_ID="${{ secrets.EI_EPHEMERAL_ORG_ID }}" || true \ No newline at end of file diff --git a/.github/workflows/pr-patch-mirror-and-apply.yml b/.github/workflows/sync-mirror.yml similarity index 82% rename from .github/workflows/pr-patch-mirror-and-apply.yml rename to .github/workflows/sync-mirror.yml index dedf01cc754..3257e014791 100644 --- a/.github/workflows/pr-patch-mirror-and-apply.yml +++ b/.github/workflows/sync-mirror.yml @@ -3,9 +3,8 @@ name: Sync to mirror run-name: sync-to-mirror-${{ github.base_ref }}-${{ github.head_ref }} on: - pull_request_target: - types: - - closed + workflow_dispatch: + push: branches: - "main" - "v*.*.*" @@ -15,10 +14,10 @@ on: jobs: trigger_downstream_patch_mirror: concurrency: patch-mirror-${{ github.ref }} - if: github.event.pull_request.merged == true uses: grafana/security-patch-actions/.github/workflows/mirror-branch-and-apply-patches.yml@main + if: github.repository == 'grafana/grafana' with: - ref: "${{ github.base_ref }}" # this is the target branch name, Ex: "main" + ref: "${{ github.ref_name }}" # this is the target branch name, Ex: "main" src_repo: "${{ github.repository }}" dest_repo: "${{ github.repository }}-security-mirror" patch_repo: "${{ github.repository }}-security-patches" diff --git a/.github/workflows/update-make-docs.yml b/.github/workflows/update-make-docs.yml new file mode 100644 index 00000000000..57d60d47c11 --- /dev/null +++ b/.github/workflows/update-make-docs.yml @@ -0,0 +1,27 @@ +name: Update `make docs` procedure +on: + schedule: + - cron: '0 7 * * 1-5' +jobs: + main: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Update procedure + if: github.repository != 'grafana/writers-toolkit' + run: | + curl -s -Lo docs/docs.mk https://raw.githubusercontent.com/grafana/writers-toolkit/main/docs/docs.mk + curl -s -Lo docs/make-docs https://raw.githubusercontent.com/grafana/writers-toolkit/main/docs/make-docs + if git diff --exit-code; then exit 0; fi + BRANCH="$(date +%Y-%m-%d)/update-make-docs" + git checkout -b "${BRANCH}" + git add . + git config --local user.email "bot@grafana.com" + git config --local user.name "grafanabot" + git commit -m "Update \`make docs\` procedure" + git push -v origin "refs/heads/${BRANCH}" + gh pr create --fill + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/conf/defaults.ini b/conf/defaults.ini index e3c86c7875a..ae8a7b0293d 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -600,6 +600,8 @@ tls_skip_verify_insecure = false tls_client_cert = tls_client_key = tls_client_ca = +# GitHub OAuth apps does not provide refresh tokens and the access tokens never expires. +use_refresh_token = false #################################### GitLab Auth ######################### [auth.gitlab] @@ -625,6 +627,7 @@ tls_client_cert = tls_client_key = tls_client_ca = use_pkce = true +use_refresh_token = true #################################### Google Auth ######################### [auth.google] @@ -647,6 +650,7 @@ tls_client_cert = tls_client_key = tls_client_ca = use_pkce = true +use_refresh_token = true #################################### Grafana.com Auth #################### # legacy key names (so they work in env variables) @@ -657,6 +661,7 @@ client_id = some_id client_secret = scopes = user:email allowed_organizations = +use_refresh_token = false [auth.grafana_com] name = Grafana.com @@ -669,6 +674,7 @@ client_secret = scopes = user:email allowed_organizations = skip_org_role_sync = false +use_refresh_token = false #################################### Azure AD OAuth ####################### [auth.azuread] @@ -694,6 +700,7 @@ tls_client_key = tls_client_ca = use_pkce = true skip_org_role_sync = false +use_refresh_token = true #################################### Okta OAuth ####################### [auth.okta] @@ -719,6 +726,7 @@ tls_client_cert = tls_client_key = tls_client_ca = use_pkce = true +use_refresh_token = false #################################### Generic OAuth ####################### [auth.generic_oauth] @@ -756,6 +764,7 @@ use_pkce = false auth_style = allow_assign_grafana_admin = false skip_org_role_sync = false +use_refresh_token = false #################################### Basic Auth ########################## [auth.basic] diff --git a/devenv/dev-dashboards/panel-timeseries/timeseries-nulls.json b/devenv/dev-dashboards/panel-timeseries/timeseries-nulls.json index b3040deb06a..89499b0d2e2 100644 --- a/devenv/dev-dashboards/panel-timeseries/timeseries-nulls.json +++ b/devenv/dev-dashboards/panel-timeseries/timeseries-nulls.json @@ -61,6 +61,7 @@ "tooltip": false, "viz": false }, + "insertNulls": false, "lineInterpolation": "linear", "lineWidth": 2, "pointSize": 7, @@ -82,7 +83,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -172,6 +174,7 @@ "tooltip": false, "viz": false }, + "insertNulls": false, "lineInterpolation": "linear", "lineWidth": 2, "pointSize": 6, @@ -194,7 +197,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -292,6 +296,7 @@ "tooltip": false, "viz": false }, + "insertNulls": false, "lineInterpolation": "linear", "lineWidth": 2, "pointSize": 6, @@ -314,7 +319,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -431,6 +437,7 @@ "tooltip": false, "viz": false }, + "insertNulls": false, "lineInterpolation": "linear", "lineWidth": 2, "pointSize": 7, @@ -452,7 +459,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -542,6 +550,7 @@ "tooltip": false, "viz": false }, + "insertNulls": false, "lineInterpolation": "linear", "lineWidth": 2, "pointSize": 6, @@ -564,7 +573,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -662,6 +672,7 @@ "tooltip": false, "viz": false }, + "insertNulls": false, "lineInterpolation": "linear", "lineWidth": 2, "pointSize": 6, @@ -684,7 +695,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -801,6 +813,7 @@ "tooltip": false, "viz": false }, + "insertNulls": false, "lineInterpolation": "linear", "lineWidth": 2, "pointSize": 5, @@ -822,7 +835,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -926,8 +940,7 @@ }, { "datasource": { - "type": "testdata", - "uid": "PD8C576611E62080A" + "type": "testdata" }, "fieldConfig": { "defaults": { @@ -948,13 +961,14 @@ "tooltip": false, "viz": false }, + "insertNulls": false, "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 8, + "lineWidth": 2, + "pointSize": 5, "scaleDistribution": { "type": "linear" }, - "showPoints": "always", + "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", @@ -969,7 +983,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -977,21 +992,54 @@ } ] }, - "unit": "short" + "unit": "celsius" }, - "overrides": [] + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "A-series" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "blue", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "C-series" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "green", + "mode": "fixed" + } + } + ] + } + ] }, "gridPos": { - "h": 6, + "h": 8, "w": 7, "x": 7, "y": 14 }, - "id": 32, + "id": 9, "options": { "legend": { + "asTable": false, "calcs": [], "displayMode": "list", + "isVisible": true, "placement": "bottom", "showLegend": true }, @@ -1000,20 +1048,42 @@ "sort": "none" } }, - "pluginVersion": "7.5.0-pre", + "pluginVersion": "7.2.0-pre", "targets": [ { "alias": "", - "csvContent": "time, val\n2023-06-14T08:16:41Z, 20\n2023-06-14T08:16:42Z, 22\n2023-06-14T08:16:43Z, 24\n2023-06-14T08:16:45Z, 10\n2023-06-14T08:16:46Z, 12\n2023-06-14T08:17:46Z, 13\n2023-06-14T08:17:47Z, 14\n2023-06-14T08:17:57Z, 20\n2023-06-14T08:17:58Z, null\n2023-06-14T08:18:00Z, 22\n2023-06-14T08:18:25Z, 22", - "datasource": { - "type": "testdata", - "uid": "PD8C576611E62080A" + "csvWave": { + "timeStep": 60, + "valuesCSV": "0,0,2,2,1,1" }, + "lines": 10, + "points": [], + "pulseWave": { + "offCount": 3, + "offValue": 1, + "onCount": 3, + "onValue": 2, + "timeStep": 60 + }, + "refId": "C", + "scenarioId": "csv_metric_values", + "stream": { + "bands": 1, + "noise": 2.2, + "speed": 250, + "spread": 3.5, + "type": "signal" + }, + "stringInput": "1,20,90,30,5,0" + }, + { "refId": "A", - "scenarioId": "csv_content" + "scenarioId": "csv_metric_values", + "stringInput": "10,25,null,null,50,10" } ], - "title": "Default (only null disconnect)", + "title": "Null values in second series show gaps (bugged)", + "transformations": [], "type": "timeseries" }, { @@ -1040,6 +1110,7 @@ "tooltip": false, "viz": false }, + "insertNulls": false, "lineInterpolation": "linear", "lineStyle": { "fill": "solid" @@ -1064,7 +1135,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -1128,6 +1200,7 @@ "tooltip": false, "viz": false }, + "insertNulls": false, "lineInterpolation": "linear", "lineStyle": { "fill": "solid" @@ -1152,7 +1225,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -1196,424 +1270,6 @@ "title": "Always show points between gaps", "type": "timeseries" }, - { - "datasource": { - "type": "datasource", - "uid": "-- Dashboard --" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 10, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 8, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "always", - "spanNulls": 10000, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "short" - }, - "overrides": [] - }, - "gridPos": { - "h": 6, - "w": 7, - "x": 7, - "y": 20 - }, - "id": 31, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "7.5.0-pre", - "targets": [ - { - "datasource": { - "type": "datasource", - "uid": "-- Dashboard --" - }, - "panelId": 32, - "refId": "A" - } - ], - "title": "Connect Nulls < 10s", - "type": "timeseries" - }, - { - "datasource": { - "type": "testdata" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 10, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineInterpolation": "linear", - "lineWidth": 2, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "celsius" - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "A-series" - }, - "properties": [ - { - "id": "color", - "value": { - "fixedColor": "blue", - "mode": "fixed" - } - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "C-series" - }, - "properties": [ - { - "id": "color", - "value": { - "fixedColor": "green", - "mode": "fixed" - } - } - ] - } - ] - }, - "gridPos": { - "h": 8, - "w": 7, - "x": 0, - "y": 22 - }, - "id": 9, - "options": { - "legend": { - "asTable": false, - "calcs": [], - "displayMode": "list", - "isVisible": true, - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "7.2.0-pre", - "targets": [ - { - "alias": "", - "csvWave": { - "timeStep": 60, - "valuesCSV": "0,0,2,2,1,1" - }, - "lines": 10, - "points": [], - "pulseWave": { - "offCount": 3, - "offValue": 1, - "onCount": 3, - "onValue": 2, - "timeStep": 60 - }, - "refId": "C", - "scenarioId": "csv_metric_values", - "stream": { - "bands": 1, - "noise": 2.2, - "speed": 250, - "spread": 3.5, - "type": "signal" - }, - "stringInput": "1,20,90,30,5,0" - }, - { - "refId": "A", - "scenarioId": "csv_metric_values", - "stringInput": "10,25,null,null,50,10" - } - ], - "title": "Null values in second series show gaps (bugged)", - "transformations": [], - "type": "timeseries" - }, - { - "datasource": { - "type": "datasource", - "uid": "-- Dashboard --" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 10, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": 59000, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 8, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "always", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "short" - }, - "overrides": [] - }, - "gridPos": { - "h": 6, - "w": 7, - "x": 7, - "y": 26 - }, - "id": 30, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "7.5.0-pre", - "targets": [ - { - "datasource": { - "type": "datasource", - "uid": "-- Dashboard --" - }, - "panelId": 32, - "refId": "A" - } - ], - "title": "Disconnect Values > 59s", - "type": "timeseries" - }, - { - "datasource": { - "type": "datasource", - "uid": "-- Dashboard --" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 10, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": 59000, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 8, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "always", - "spanNulls": 10000, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "short" - }, - "overrides": [] - }, - "gridPos": { - "h": 6, - "w": 7, - "x": 7, - "y": 32 - }, - "id": 29, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "7.5.0-pre", - "targets": [ - { - "datasource": { - "type": "datasource", - "uid": "-- Dashboard --" - }, - "panelId": 32, - "refId": "A" - } - ], - "title": "Connect Nulls < 10s, Disconnect Values > 59s", - "type": "timeseries" - }, { "datasource": { "type": "testdata", @@ -1638,6 +1294,7 @@ "tooltip": false, "viz": false }, + "insertNulls": false, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, @@ -1659,7 +1316,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -1674,7 +1332,7 @@ "h": 11, "w": 24, "x": 0, - "y": 38 + "y": 22 }, "id": 18, "maxDataPoints": 150, @@ -1728,6 +1386,7 @@ "tooltip": false, "viz": false }, + "insertNulls": false, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, @@ -1749,7 +1408,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -1764,7 +1424,7 @@ "h": 10, "w": 12, "x": 0, - "y": 49 + "y": 33 }, "id": 20, "maxDataPoints": 150, @@ -1818,6 +1478,7 @@ "tooltip": false, "viz": false }, + "insertNulls": false, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, @@ -1839,7 +1500,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -1854,7 +1516,7 @@ "h": 10, "w": 12, "x": 12, - "y": 49 + "y": 33 }, "id": 22, "maxDataPoints": 150, @@ -1908,6 +1570,7 @@ "tooltip": false, "viz": false }, + "insertNulls": false, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, @@ -1929,7 +1592,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -1944,7 +1608,7 @@ "h": 10, "w": 8, "x": 0, - "y": 59 + "y": 43 }, "id": 24, "maxDataPoints": 150, @@ -1998,6 +1662,7 @@ "tooltip": false, "viz": false }, + "insertNulls": false, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, @@ -2019,7 +1684,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -2034,7 +1700,7 @@ "h": 10, "w": 8, "x": 8, - "y": 59 + "y": 43 }, "id": 26, "maxDataPoints": 150, @@ -2088,6 +1754,7 @@ "tooltip": false, "viz": false }, + "insertNulls": false, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, @@ -2109,7 +1776,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -2124,7 +1792,7 @@ "h": 10, "w": 8, "x": 16, - "y": 59 + "y": 43 }, "id": 28, "maxDataPoints": 150, @@ -2153,15 +1821,1121 @@ ], "title": "Higher density (mostly nulls, no leading/trailing)", "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 53 + }, + "id": 34, + "panels": [], + "title": "Connect nulls + disconnect values", + "type": "row" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 8, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "always", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 7, + "x": 0, + "y": 54 + }, + "id": 32, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "7.5.0-pre", + "targets": [ + { + "alias": "", + "csvContent": "time, val\n2023-06-14T08:16:41Z, 20\n2023-06-14T08:16:42Z, 22\n2023-06-14T08:16:43Z, 24\n2023-06-14T08:16:45Z, 10\n2023-06-14T08:16:46Z, 12\n2023-06-14T08:17:46Z, 13\n2023-06-14T08:17:47Z, 14\n2023-06-14T08:17:57Z, 20\n2023-06-14T08:17:58Z, null\n2023-06-14T08:18:00Z, 22\n2023-06-14T08:18:25Z, 22", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "scenarioId": "csv_content" + } + ], + "title": "Default (only null disconnect)", + "type": "timeseries" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 7, + "x": 7, + "y": 54 + }, + "id": 35, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "7.5.0-pre", + "targets": [ + { + "alias": "", + "csvContent": "x, val\n1, 20\n2, 22\n3, 24\n5, 10\n6, 12\n66, 13\n67, 14\n77, 20\n78, null\n80, 22\n105, 22", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "scenarioId": "csv_content" + } + ], + "title": "Default (only null disconnect)", + "type": "trend" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "#EAB839", + "value": 10 + }, + { + "color": "#6ED0E0", + "value": 20 + }, + { + "color": "#EF843C", + "value": 22 + }, + { + "color": "#E24D42", + "value": 24 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 10, + "x": 14, + "y": 54 + }, + "id": 39, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "7.5.0-pre", + "targets": [ + { + "alias": "", + "csvContent": "time, val\n2023-06-14T08:16:41Z, 20\n2023-06-14T08:16:42Z, 22\n2023-06-14T08:16:43Z, 24\n2023-06-14T08:16:45Z, 10\n2023-06-14T08:16:46Z, 12\n2023-06-14T08:17:46Z, 13\n2023-06-14T08:17:47Z, 14\n2023-06-14T08:17:57Z, 20\n2023-06-14T08:17:58Z, null\n2023-06-14T08:18:00Z, 22\n2023-06-14T08:18:25Z, 22", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "scenarioId": "csv_content" + } + ], + "title": "Default (only null disconnect)", + "type": "state-timeline" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 8, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "always", + "spanNulls": 10000, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 7, + "x": 0, + "y": 60 + }, + "id": 31, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "7.5.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 32, + "refId": "A" + } + ], + "title": "Connect Nulls < 10s", + "type": "timeseries" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": 10, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 7, + "x": 7, + "y": 60 + }, + "id": 36, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "7.5.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 35, + "refId": "A" + } + ], + "title": "Connect Nulls < 10", + "type": "trend" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 0, + "spanNulls": 10000 + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "#EAB839", + "value": 10 + }, + { + "color": "#6ED0E0", + "value": 20 + }, + { + "color": "#EF843C", + "value": 22 + }, + { + "color": "#E24D42", + "value": 24 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 10, + "x": 14, + "y": 60 + }, + "id": 40, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "7.5.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 32, + "refId": "A" + } + ], + "title": "Connect Nulls < 10s", + "type": "state-timeline" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": 59000, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 8, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "always", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 7, + "x": 0, + "y": 66 + }, + "id": 30, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "7.5.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 32, + "refId": "A" + } + ], + "title": "Disconnect Values > 59s", + "type": "timeseries" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": 59, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 7, + "x": 7, + "y": 66 + }, + "id": 37, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "7.5.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 35, + "refId": "A" + } + ], + "title": "Disconnect Values > 59", + "type": "trend" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": 59000, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "#EAB839", + "value": 10 + }, + { + "color": "#6ED0E0", + "value": 20 + }, + { + "color": "#EF843C", + "value": 22 + }, + { + "color": "#E24D42", + "value": 24 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 10, + "x": 14, + "y": 66 + }, + "id": 41, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "7.5.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 32, + "refId": "A" + } + ], + "title": "Disconnect Values > 59s", + "type": "state-timeline" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": 59000, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 8, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "always", + "spanNulls": 10000, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 7, + "x": 0, + "y": 72 + }, + "id": 29, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "7.5.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 32, + "refId": "A" + } + ], + "title": "Connect Nulls < 10s, Disconnect Values > 59s", + "type": "timeseries" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": 59, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": 10, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 7, + "x": 7, + "y": 72 + }, + "id": 38, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "7.5.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 35, + "refId": "A" + } + ], + "title": "Connect Nulls < 10, Disconnect Values > 59", + "type": "trend" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": 59000, + "lineWidth": 0, + "spanNulls": 10000 + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "#EAB839", + "value": 10 + }, + { + "color": "#6ED0E0", + "value": 20 + }, + { + "color": "#EF843C", + "value": 22 + }, + { + "color": "#E24D42", + "value": 24 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 10, + "x": 14, + "y": 72 + }, + "id": 42, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "7.5.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 32, + "refId": "A" + } + ], + "title": "Connect Nulls < 10s, Disconnect Values > 59s", + "type": "state-timeline" } ], - "refresh": false, + "refresh": "", "schemaVersion": 38, "style": "dark", "tags": [ "gdev", "panel-tests", - "graph-ng" + "graph-ng", + "timeseries", + "trend", + "state-timeline" ], "templating": { "list": [] @@ -2174,6 +2948,6 @@ "timezone": "", "title": "Panel Tests - Graph NG - Gaps and Connected", "uid": "8mmCAF1Mz", - "version": 10, + "version": 5, "weekStart": "" -} +} \ No newline at end of file diff --git a/docs/make-docs b/docs/make-docs index 3942bf483c3..4bb5ad79038 100755 --- a/docs/make-docs +++ b/docs/make-docs @@ -63,20 +63,23 @@ SOURCES_as_code='as-code-docs' SOURCES_enterprise_metrics='backend-enterprise' SOURCES_enterprise_metrics_='backend-enterprise' SOURCES_grafana_cloud='website' +SOURCES_grafana_cloud_alerting_and_irm_machine_learning='machine-learning' +SOURCES_grafana_cloud_alerting_and_irm_slo='slo' SOURCES_grafana_cloud_k6='k6-docs' SOURCES_grafana_cloud_data_configuration_integrations='cloud-onboarding' SOURCES_grafana_cloud_frontend_observability_faro_web_sdk='faro-web-sdk' -SOURCES_grafana_cloud_machine_learning='machine-learning' SOURCES_helm_charts_mimir_distributed='mimir' SOURCES_helm_charts_tempo_distributed='tempo' SOURCES_opentelemetry='opentelemetry-docs' +SOURCES_plugins_grafana_splunk_datasource='splunk-datasource' VERSIONS_as_code='UNVERSIONED' VERSIONS_grafana_cloud='UNVERSIONED' +VERSIONS_grafana_cloud_alerting_and_irm_machine_learning='UNVERSIONED' +VERSIONS_grafana_cloud_alerting_and_irm_slo='UNVERSIONED' VERSIONS_grafana_cloud_k6='UNVERSIONED' VERSIONS_grafana_cloud_data_configuration_integrations='UNVERSIONED' VERSIONS_grafana_cloud_frontend_observability_faro_web_sdk='UNVERSIONED' -VERSIONS_grafana_cloud_machine_learning='UNVERSIONED' VERSIONS_opentelemetry='UNVERSIONED' VERSIONS_technical_documentation='UNVERSIONED' VERSIONS_website='UNVERSIONED' diff --git a/docs/sources/administration/plugin-management/index.md b/docs/sources/administration/plugin-management/index.md index 2ae610d07cf..127e0a05a86 100644 --- a/docs/sources/administration/plugin-management/index.md +++ b/docs/sources/administration/plugin-management/index.md @@ -19,7 +19,7 @@ Grafana supports three types of plugins: [panels](/grafana/plugins?type=panel), ## Panel plugins -Add new visualizations to your dashboard with panel plugins, such as the [Worldmap Panel](/grafana/plugins/grafana-worldmap-panel), [Clock](/grafana/plugins/grafana-clock-panel), and [Pie Chart](/grafana/plugins/grafana-piechart-panel). +Add new visualizations to your dashboard with panel plugins, such as the [Clock](/grafana/plugins/grafana-clock-panel), [Mosaic](/grafana/plugins/boazreicher-mosaicplot-panel) and [Variable](/grafana/plugins/volkovlabs-variable-panel) panels. Use panel plugins when you want to: diff --git a/docs/sources/developers/kinds/core/dashboard/schema-reference.md b/docs/sources/developers/kinds/core/dashboard/schema-reference.md index 17b617d931f..8b3206a752e 100644 --- a/docs/sources/developers/kinds/core/dashboard/schema-reference.md +++ b/docs/sources/developers/kinds/core/dashboard/schema-reference.md @@ -301,11 +301,11 @@ For example, if a value is within a certain range, you can configure a range val Range to match against and the result to apply when the value is within the range -| Property | Type | Required | Default | Description | -|----------|-------------------------------------------|----------|---------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `from` | number or null | **Yes** | | Min value of the range. It can be null which means -Infinity
Constraint: `>=-1.797693134862315708145274237317043567981E+308 & <=1.797693134862315708145274237317043567981E+308`. | -| `result` | [ValueMappingResult](#valuemappingresult) | **Yes** | | Result used as replacement with text and color when the value matches | -| `to` | number or null | **Yes** | | Max value of the range. It can be null which means +Infinity
Constraint: `>=-1.797693134862315708145274237317043567981E+308 & <=1.797693134862315708145274237317043567981E+308`. | +| Property | Type | Required | Default | Description | +|----------|-------------------------------------------|----------|---------|-----------------------------------------------------------------------| +| `from` | number or null | **Yes** | | Min value of the range. It can be null which means -Infinity | +| `result` | [ValueMappingResult](#valuemappingresult) | **Yes** | | Result used as replacement with text and color when the value matches | +| `to` | number or null | **Yes** | | Max value of the range. It can be null which means +Infinity | ### ValueMappingResult diff --git a/docs/sources/developers/plugins/create-a-grafana-plugin/develop-a-plugin/build-an-app-plugin.md b/docs/sources/developers/plugins/create-a-grafana-plugin/develop-a-plugin/build-an-app-plugin.md index cd77168132b..2ebbd79d1da 100644 --- a/docs/sources/developers/plugins/create-a-grafana-plugin/develop-a-plugin/build-an-app-plugin.md +++ b/docs/sources/developers/plugins/create-a-grafana-plugin/develop-a-plugin/build-an-app-plugin.md @@ -194,9 +194,9 @@ If you want to let users know that your app requires an existing plugin, you can "plugins": [ { "type": "panel", - "name": "Worldmap Panel", - "id": "grafana-worldmap-panel", - "version": "^0.3.2" + "name": "Clock Panel", + "id": "grafana-clock-panel", + "version": "^2.1.3" } ] } diff --git a/docs/sources/panels-visualizations/visualizations/state-timeline/index.md b/docs/sources/panels-visualizations/visualizations/state-timeline/index.md index 83f652dc175..151ab5a5010 100644 --- a/docs/sources/panels-visualizations/visualizations/state-timeline/index.md +++ b/docs/sources/panels-visualizations/visualizations/state-timeline/index.md @@ -46,6 +46,10 @@ Controls line width of state regions. Controls the opacity of state regions. +{{< docs/shared "visualizations/connect-null-values.md" >}} + +{{< docs/shared "visualizations/disconnect-values.md" >}} + ## Value mappings To assign colors to boolean or string values, you can use [Value mappings]({{< relref "../../configure-value-mappings/" >}}). diff --git a/docs/sources/panels-visualizations/visualizations/time-series/index.md b/docs/sources/panels-visualizations/visualizations/time-series/index.md index d5025acf86a..2b3e56758f7 100644 --- a/docs/sources/panels-visualizations/visualizations/time-series/index.md +++ b/docs/sources/panels-visualizations/visualizations/time-series/index.md @@ -131,15 +131,9 @@ Set the style of the line. To change the color, use the standard [color scheme]( ![Line styles examples](/static/img/docs/time-series-panel/line-styles-examples-v9.png) -### Connect null values +{{< docs/shared "visualizations/connect-null-values.md" >}} -Choose how null values, which are gaps in the data, appear on the graph. Null values can be connected to form a continuous line or set to a threshold above which gaps in the data are no longer connected. - -![Connect null values option](/static/img/docs/time-series-panel/connect-null-values-option-v9.png) - -- **Never:** Time series data points with gaps in the the data are never connected. -- **Always:** Time series data points with gaps in the the data are always connected. -- **Threshold:** Specify a threshold above which gaps in the data are no longer connected. This can be useful when the connected gaps in the data are of a known size and/or within a known range, and gaps outside this range should no longer be connected. +{{< docs/shared "visualizations/disconnect-values.md" >}} ### Stack series diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index 4af59af6325..97a17d8f58d 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -827,7 +827,9 @@ Sets the default UI theme: `dark`, `light`, or `system`. The default theme is `d ### default_language -This setting configures the default UI language, which must be a supported IETF language tag, such as `en-US`. +This option will set the default UI language if a supported IETF language tag like `en-US` is available. +If set to `detect`, the default UI language will be determined by browser preference. +The default is `en-US`. ### home_page diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 61abf60d29c..df8a74dbbb2 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -126,6 +126,7 @@ Experimental features might be changed or removed without prior notice. | `logsExploreTableVisualisation` | A table visualisation for logs in Explore | | `awsDatasourcesTempCredentials` | Support temporary security credentials in AWS plugins for Grafana Cloud customers | | `transformationsRedesign` | Enables the transformations redesign | +| `mlExpressions` | Enable support for Machine Learning in server-side expressions | ## Development feature toggles diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/azuread/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/azuread/index.md index 7429a408953..f99349fddd8 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/azuread/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/azuread/index.md @@ -21,6 +21,7 @@ The Azure AD authentication allows you to use an Azure Active Directory tenant a - [Assign server administrator privileges](#assign-server-administrator-privileges) - [Enable Azure AD OAuth in Grafana](#enable-azure-ad-oauth-in-grafana) - [Configure refresh token](#configure-refresh-token) + - [Configure allowed tenants](#configure-allowed-tenants) - [Configure allowed groups](#configure-allowed-groups) - [Configure allowed domains](#configure-allowed-domains) - [PKCE](#pkce) @@ -176,7 +177,9 @@ When a user logs in using an OAuth provider, Grafana verifies that the access to Grafana uses a refresh token to obtain a new access token without requiring the user to log in again. If a refresh token doesn't exist, Grafana logs the user out of the system after the access token has expired. -To enable a refresh token for AzureAD, extend the `scopes` in `[auth.azuread]` with `offline_access`. +Refresh token fetching and access token expiration check is enabled by default for the AzureAD provider since Grafana v10.1.0 if the `accessTokenExpirationCheck` feature toggle is enabled. If you would like to disable access token expiration check then set the `use_refresh_token` configuration value to `false`. + +> **Note:** The `accessTokenExpirationCheck` feature toggle will be removed in Grafana v10.2.0 and the `use_refresh_token` configuration value will be used instead for configuring refresh token fetching and access token expiration check. ### Configure allowed tenants diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/generic-oauth/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/generic-oauth/index.md index 6bdf565ca02..17aa7a95ed3 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/generic-oauth/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/generic-oauth/index.md @@ -65,7 +65,9 @@ To integrate your OAuth2 provider with Grafana using our generic OAuth2 authenti b. Extend the `scopes` field of `[auth.generic_oauth]` section in Grafana configuration file with refresh token scope used by your OAuth2 provider. - c. Enable the refresh token on the provider if required. + c. Set `use_refresh_token` to `true` in `[auth.generic_oauth]` section in Grafana configuration file. + + d. Enable the refresh token on the provider if required. 1. [Configure role mapping]({{< relref "#configure-role-mapping" >}}). 1. Optional: [Configure team synchronization]({{< relref "#configure-team-synchronization" >}}). @@ -113,6 +115,7 @@ The following table outlines the various generic OAuth2 configuration options. Y | `tls_client_key` | No | The path to the key. | | | `tls_client_ca` | No | The path to the trusted certificate authority list. | | | `use_pkce` | No | Set to `true` to use [Proof Key for Code Exchange (PKCE)](https://datatracker.ietf.org/doc/html/rfc7636). Grafana uses the SHA256 based `S256` challenge method and a 128 bytes (base64url encoded) code verifier. | `false` | +| `use_refresh_token` | No | Set to `true` to use refresh token and check access token expiration. The `accessTokenExpirationCheck` feature toggle should also be enabled to use refresh token. | `false` | ### Configure login @@ -169,11 +172,13 @@ When a user logs in using an OAuth2 provider, Grafana verifies that the access t Grafana uses a refresh token to obtain a new access token without requiring the user to log in again. If a refresh token doesn't exist, Grafana logs the user out of the system after the access token has expired. -To configure generic OAuth2 to use a refresh token, perform one or both of the following steps, if required: +To configure generic OAuth2 to use a refresh token, set `use_refresh_token` configuration option to `true` and perform one or both of the following steps, if required: 1. Extend the `scopes` field of `[auth.generic_oauth]` section in Grafana configuration file with additional scopes. 1. Enable the refresh token on the provider. +> **Note:** The `accessTokenExpirationCheck` feature toggle will be removed in Grafana v10.2.0 and the `use_refresh_token` configuration value will be used instead for configuring refresh token fetching and access token expiration check. + ## Configure role mapping Unless `skip_org_role_sync` option is enabled, the user's role will be set to the role retrieved from the auth provider upon user login. @@ -336,6 +341,7 @@ To set up generic OAuth2 authentication with Auth0, follow these steps: token_url = https:///oauth/token api_url = https:///userinfo use_pkce = true + use_refresh_token = true ``` ### Set up OAuth2 with Bitbucket @@ -368,6 +374,7 @@ To set up generic OAuth2 authentication with Bitbucket, follow these steps: team_ids_attribute_path = values[*].workspace.slug team_ids = allowed_organizations = + use_refresh_token = true ``` By default, a refresh token is included in the response for the **Authorization Code Grant**. diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/gitlab/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/gitlab/index.md index 0f3d64baf69..fb17147a693 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/gitlab/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/gitlab/index.md @@ -104,6 +104,10 @@ Grafana uses a refresh token to obtain a new access token without requiring the By default, GitLab provides a refresh token. +Refresh token fetching and access token expiration check is enabled by default for the GitLab provider since Grafana v10.1.0 if the `accessTokenExpirationCheck` feature toggle is enabled. If you would like to disable access token expiration check then set the `use_refresh_token` configuration value to `false`. + +> **Note:** The `accessTokenExpirationCheck` feature toggle will be removed in Grafana v10.2.0 and the `use_refresh_token` configuration value will be used instead for configuring refresh token fetching and access token expiration check. + ### allowed_groups To limit access to authenticated users that are members of one or more [GitLab diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/google/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/google/index.md index b9538abe406..12759606d8d 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/google/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/google/index.md @@ -82,6 +82,10 @@ Grafana uses a refresh token to obtain a new access token without requiring the By default, Grafana includes the `access_type=offline` parameter in the authorization request to request a refresh token. +Refresh token fetching and access token expiration check is enabled by default for the Google provider since Grafana v10.1.0 if the `accessTokenExpirationCheck` feature toggle is enabled. If you would like to disable access token expiration check then set the `use_refresh_token` configuration value to `false`. + +> **Note:** The `accessTokenExpirationCheck` feature toggle will be removed in Grafana v10.2.0 and the `use_refresh_token` configuration value will be used instead for configuring refresh token fetching and access token expiration check. + ### Configure automatic login Set `auto_login` option to true to attempt login automatically, skipping the login screen. diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/okta/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/okta/index.md index edfb4209504..3f712de21dc 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/okta/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/okta/index.md @@ -75,7 +75,10 @@ When a user logs in using an OAuth provider, Grafana verifies that the access to Grafana uses a refresh token to obtain a new access token without requiring the user to log in again. If a refresh token doesn't exist, Grafana logs the user out of the system after the access token has expired. 1. To enable the `Refresh Token`, grant type in the `General Settings` section. -1. Extend the `scopes` in `[auth.okta]` with `offline_access`. +1. Extend the `scopes` in `[auth.okta]` with `offline_access` for Grafana versions between v9.3 and v10.0.x. +1. Set `use_refresh_token` in `[auth.okta]` to `true` for Grafana versions v10.1.0 and later. + +> **Note:** The `accessTokenExpirationCheck` feature toggle will be removed in Grafana v10.2.0 and the `use_refresh_token` configuration value will be used instead for configuring refresh token fetching and access token expiration check. ### Configure allowed groups and domains diff --git a/docs/sources/shared/visualizations/connect-null-values.md b/docs/sources/shared/visualizations/connect-null-values.md new file mode 100644 index 00000000000..4336568cd39 --- /dev/null +++ b/docs/sources/shared/visualizations/connect-null-values.md @@ -0,0 +1,13 @@ +--- +title: Connect null values +--- + +### Connect null values + +Choose how null values, which are gaps in the data, appear on the graph. Null values can be connected to form a continuous line or set to a threshold above which gaps in the data are no longer connected. + +![Connect null values option](/static/img/docs/time-series-panel/connect-null-values-option-v9.png) + +- **Never:** Time series data points with gaps in the the data are never connected. +- **Always:** Time series data points with gaps in the the data are always connected. +- **Threshold:** Specify a threshold above which gaps in the data are no longer connected. This can be useful when the connected gaps in the data are of a known size and/or within a known range, and gaps outside this range should no longer be connected. diff --git a/docs/sources/shared/visualizations/disconnect-values.md b/docs/sources/shared/visualizations/disconnect-values.md new file mode 100644 index 00000000000..6786ba43c30 --- /dev/null +++ b/docs/sources/shared/visualizations/disconnect-values.md @@ -0,0 +1,12 @@ +--- +title: Disconnect values +--- + +### Disconnect values + +Choose whether to set a threshold above which values in the data should be disconnected. + +{{< figure src="/media/docs/grafana/screenshot-grafana-10-1-disconnect-values.png" max-width="750px" >}} + +- **Never:** Time series data points in the the data are never disconnected. +- **Threshold:** Specify a threshold above which values in the data are disconnected. This can be useful when desired values in the data are of a known size and/or within a known range, and values outside this range should no longer be connected. diff --git a/docs/variables.mk b/docs/variables.mk index 2dfa301900c..a3302041ce5 100644 --- a/docs/variables.mk +++ b/docs/variables.mk @@ -1,11 +1,11 @@ # List of projects to provide to the make-docs script. -PROJECTS = grafana +PROJECTS := grafana # Use the doc-validator image defined in CI by default. export DOC_VALIDATOR_IMAGE := $(shell sed -En 's, *image: "(grafana/doc-validator[^"]+)",\1,p' "$(shell git rev-parse --show-toplevel)/.github/workflows/doc-validator.yml") # Skip some doc-validator checks. -export DOC_VALIDATOR_SKIP_CHECKS := ^(?:image.+|canonical-does-not-match-pretty-URL)$ +export DOC_VALIDATOR_SKIP_CHECKS := $(shell sed -En "s, *'--skip-checks=(.+)',\1,p" "$(shell git rev-parse --show-toplevel)/.github/workflows/doc-validator.yml") -# Use alternative image until make-docs 3.0.0 is rolled out. -export DOCS_IMAGE := grafana/docs-base:dbd975af06 +# Only run on sections that have been enabled in CI. +export DOC_VALIDATOR_INCLUDE := $(shell sed -En "s, *'--include=\\^docs/sources/(.+)',/hugo/content/docs/grafana/latest/\1,p" "$(shell git rev-parse --show-toplevel)/.github/workflows/doc-validator.yml") diff --git a/e2e/panels-suite/panelEdit_transforms.spec.ts b/e2e/panels-suite/panelEdit_transforms.spec.ts index 6e45dd6522a..6f12a438c73 100644 --- a/e2e/panels-suite/panelEdit_transforms.spec.ts +++ b/e2e/panels-suite/panelEdit_transforms.spec.ts @@ -10,10 +10,7 @@ e2e.scenario({ e2e.flows.openDashboard({ uid: '5SdHCadmz', queryParams: { editPanel: 3 } }); e2e.components.Tab.title('Transform').should('be.visible').click(); - - // Flacky tests. Error: cy.click() failed because this element is detached from the DOM. - // The element is visible and clickable manually. - // e2e.components.TransformTab.newTransform('Reduce').scrollIntoView().should('be.visible').click(); - // e2e.components.Transforms.Reduce.calculationsLabel().should('be.visible'); + e2e.components.TransformTab.newTransform('Reduce').scrollIntoView().should('be.visible').click(); + e2e.components.Transforms.Reduce.calculationsLabel().should('be.visible'); }, }); diff --git a/go.mod b/go.mod index 3d1ab434046..afe47307222 100644 --- a/go.mod +++ b/go.mod @@ -13,13 +13,13 @@ replace github.com/denisenkom/go-mssqldb => github.com/grafana/go-mssqldb v0.9.2 replace github.com/docker/docker => github.com/moby/moby v23.0.4+incompatible // contains openapi encoder fixes. remove ASAP -replace cuelang.org/go => github.com/sdboyer/cue v0.5.0-beta.2.0.20230419165817-251c3ae823d8 +replace cuelang.org/go => github.com/sdboyer/cue v0.5.0-beta.2.0.20230712135403-bdc4772ae055 // For some insane reason, client-go seems to have a broken v12.0.0 tag on it that forces us to // hoist a replace statement. replace k8s.io/client-go => k8s.io/client-go v0.25.3 -require k8s.io/apimachinery v0.26.2 +require k8s.io/apimachinery v0.26.2 // @grafana/backend-platform // Override Prometheus version because Prometheus v2.X is tagged as v0.X for Go modules purposes and Go assumes // that v1.Y is higher than v0.X, so when we resolve dependencies if any dependency imports v1.Y we'd @@ -27,104 +27,104 @@ require k8s.io/apimachinery v0.26.2 replace github.com/prometheus/prometheus => github.com/prometheus/prometheus v0.43.0 require ( - cloud.google.com/go/storage v1.28.1 - cuelang.org/go v0.6.0-0.dev - github.com/Azure/azure-sdk-for-go v65.0.0+incompatible - github.com/Azure/go-autorest/autorest v0.11.28 - github.com/BurntSushi/toml v1.2.1 - github.com/Masterminds/semver v1.5.0 - github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f - github.com/aws/aws-sdk-go v1.44.234 - github.com/beevik/etree v1.2.0 - github.com/benbjohnson/clock v1.3.3 - github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b - github.com/centrifugal/centrifuge v0.29.1 - github.com/crewjam/saml v0.4.13 - github.com/fatih/color v1.15.0 - github.com/gchaincl/sqlhooks v1.3.0 - github.com/go-git/go-git/v5 v5.4.2 - github.com/go-ldap/ldap/v3 v3.4.4 - github.com/go-openapi/strfmt v0.21.7 - github.com/go-redis/redis/v8 v8.11.5 - github.com/go-sourcemap/sourcemap v2.1.3+incompatible - github.com/go-sql-driver/mysql v1.7.1 - github.com/go-stack/stack v1.8.1 - github.com/gobwas/glob v0.2.3 + cloud.google.com/go/storage v1.28.1 // @grafana/backend-platform + cuelang.org/go v0.6.0-0.dev // @grafana/grafana-as-code + github.com/Azure/azure-sdk-for-go v65.0.0+incompatible // @grafana/backend-platform + github.com/Azure/go-autorest/autorest v0.11.28 // @grafana/backend-platform + github.com/BurntSushi/toml v1.2.1 // @grafana/grafana-authnz-team + github.com/Masterminds/semver v1.5.0 // @grafana/backend-platform + github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f // @grafana/backend-platform + github.com/aws/aws-sdk-go v1.44.234 // @grafana/aws-datasources + github.com/beevik/etree v1.2.0 // @grafana/backend-platform + github.com/benbjohnson/clock v1.3.3 // @grafana/alerting-squad-backend + github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b // @grafana/backend-platform + github.com/centrifugal/centrifuge v0.29.1 // @grafana/grafana-app-platform-squad + github.com/crewjam/saml v0.4.13 // @grafana/backend-platform + github.com/fatih/color v1.15.0 // @grafana/backend-platform + github.com/gchaincl/sqlhooks v1.3.0 // @grafana/backend-platform + github.com/go-git/go-git/v5 v5.4.2 // @grafana/grafana-app-platform-squad + github.com/go-ldap/ldap/v3 v3.4.4 // @grafana/grafana-authnz-team + github.com/go-openapi/strfmt v0.21.7 // @grafana/alerting-squad-backend + github.com/go-redis/redis/v8 v8.11.5 // @grafana/backend-platform + github.com/go-sourcemap/sourcemap v2.1.3+incompatible // @grafana/backend-platform + github.com/go-sql-driver/mysql v1.7.1 // @grafana/backend-platform + github.com/go-stack/stack v1.8.1 // @grafana/backend-platform + github.com/gobwas/glob v0.2.3 // @grafana/backend-platform github.com/gofrs/uuid v4.4.0+incompatible // indirect - github.com/gogo/protobuf v1.3.2 - github.com/golang/mock v1.6.0 - github.com/golang/snappy v0.0.4 - github.com/google/go-cmp v0.5.9 - github.com/google/uuid v1.3.0 - github.com/google/wire v0.5.0 - github.com/gorilla/websocket v1.5.0 - github.com/grafana/alerting v0.0.0-20230606080147-55b8d71c7890 - github.com/grafana/cuetsy v0.1.9 - github.com/grafana/grafana-aws-sdk v0.15.0 - github.com/grafana/grafana-azure-sdk-go v1.7.0 - github.com/grafana/grafana-plugin-sdk-go v0.165.0 - github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 - github.com/hashicorp/go-hclog v1.5.0 - github.com/hashicorp/go-plugin v1.4.9 - github.com/hashicorp/go-version v1.3.0 - github.com/influxdata/influxdb-client-go/v2 v2.6.0 - github.com/influxdata/line-protocol v0.0.0-20210311194329-9aa0e372d097 - github.com/jmespath/go-jmespath v0.4.0 - github.com/json-iterator/go v1.1.12 - github.com/jung-kurt/gofpdf v1.16.2 - github.com/lib/pq v1.10.6 - github.com/linkedin/goavro/v2 v2.10.0 - github.com/m3db/prometheus_remote_client_golang v0.4.4 - github.com/magefile/mage v1.14.0 - github.com/mattn/go-isatty v0.0.18 - github.com/mattn/go-sqlite3 v1.14.16 - github.com/matttproud/golang_protobuf_extensions v1.0.4 - github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f + github.com/gogo/protobuf v1.3.2 // @grafana/alerting-squad-backend + github.com/golang/mock v1.6.0 // @grafana/alerting-squad-backend + github.com/golang/snappy v0.0.4 // @grafana/alerting-squad-backend + github.com/google/go-cmp v0.5.9 // @grafana/backend-platform + github.com/google/uuid v1.3.0 // @grafana/backend-platform + github.com/google/wire v0.5.0 // @grafana/backend-platform + github.com/gorilla/websocket v1.5.0 // @grafana/grafana-app-platform-squad + github.com/grafana/alerting v0.0.0-20230606080147-55b8d71c7890 // @grafana/alerting-squad-backend + github.com/grafana/cuetsy v0.1.10 // @grafana/grafana-as-code + github.com/grafana/grafana-aws-sdk v0.15.0 // @grafana/aws-datasources + github.com/grafana/grafana-azure-sdk-go v1.7.0 // @grafana/backend-platform + github.com/grafana/grafana-plugin-sdk-go v0.165.0 // @grafana/alerting-squad-backend + github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // @grafana/backend-platform + github.com/hashicorp/go-hclog v1.5.0 // @grafana/plugins-platform-backend + github.com/hashicorp/go-plugin v1.4.9 // @grafana/plugins-platform-backend + github.com/hashicorp/go-version v1.3.0 // @grafana/backend-platform + github.com/influxdata/influxdb-client-go/v2 v2.6.0 // @grafana/observability-metrics + github.com/influxdata/line-protocol v0.0.0-20210311194329-9aa0e372d097 // @grafana/grafana-app-platform-squad + github.com/jmespath/go-jmespath v0.4.0 // @grafana/backend-platform + github.com/json-iterator/go v1.1.12 // @grafana/backend-platform + github.com/jung-kurt/gofpdf v1.16.2 // @grafana/backend-platform + github.com/lib/pq v1.10.6 // @grafana/backend-platform + github.com/linkedin/goavro/v2 v2.10.0 // @grafana/backend-platform + github.com/m3db/prometheus_remote_client_golang v0.4.4 // @grafana/backend-platform + github.com/magefile/mage v1.14.0 // @grafana/grafana-delivery + github.com/mattn/go-isatty v0.0.18 // @grafana/backend-platform + github.com/mattn/go-sqlite3 v1.14.16 // @grafana/backend-platform + github.com/matttproud/golang_protobuf_extensions v1.0.4 // @grafana/alerting-squad-backend + github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // @grafana/grafana-operator-experience-squad github.com/opentracing/opentracing-go v1.2.0 // indirect - github.com/patrickmn/go-cache v2.1.0+incompatible + github.com/patrickmn/go-cache v2.1.0+incompatible // @grafana/alerting-squad-backend github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/prometheus/alertmanager v0.25.0 - github.com/prometheus/client_golang v1.15.1 - github.com/prometheus/client_model v0.4.0 - github.com/prometheus/common v0.43.0 - github.com/prometheus/prometheus v1.8.2-0.20210621150501-ff58416a0b02 - github.com/robfig/cron/v3 v3.0.1 - github.com/russellhaering/goxmldsig v1.4.0 - github.com/stretchr/testify v1.8.4 - github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf - github.com/ua-parser/uap-go v0.0.0-20211112212520-00c877edfe0f + github.com/prometheus/alertmanager v0.25.0 // @grafana/alerting-squad-backend + github.com/prometheus/client_golang v1.15.1 // @grafana/alerting-squad-backend + github.com/prometheus/client_model v0.4.0 // @grafana/backend-platform + github.com/prometheus/common v0.43.0 // @grafana/alerting-squad-backend + github.com/prometheus/prometheus v1.8.2-0.20210621150501-ff58416a0b02 // @grafana/alerting-squad-backend + github.com/robfig/cron/v3 v3.0.1 // @grafana/backend-platform + github.com/russellhaering/goxmldsig v1.4.0 // @grafana/backend-platform + github.com/stretchr/testify v1.8.4 // @grafana/backend-platform + github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf // @grafana/backend-platform + github.com/ua-parser/uap-go v0.0.0-20211112212520-00c877edfe0f // @grafana/backend-platform github.com/uber/jaeger-client-go v2.29.1+incompatible // indirect - github.com/urfave/cli/v2 v2.25.0 - github.com/vectordotdev/go-datemath v0.1.1-0.20220323213446-f3954d0b18ae - github.com/yalue/merged_fs v1.2.2 - github.com/yudai/gojsondiff v1.0.0 - go.opentelemetry.io/collector v0.31.0 - go.opentelemetry.io/collector/model v0.31.0 - go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.37.0 - go.opentelemetry.io/otel v1.14.0 - go.opentelemetry.io/otel/exporters/jaeger v1.0.0 - go.opentelemetry.io/otel/sdk v1.14.0 - go.opentelemetry.io/otel/trace v1.14.0 - golang.org/x/crypto v0.11.0 - golang.org/x/exp v0.0.0-20230321023759-10a507213a29 - golang.org/x/net v0.10.0 - golang.org/x/oauth2 v0.7.0 - golang.org/x/sync v0.3.0 - golang.org/x/time v0.3.0 - golang.org/x/tools v0.7.0 - gonum.org/v1/gonum v0.11.0 - google.golang.org/api v0.111.0 - google.golang.org/grpc v1.55.0 - google.golang.org/protobuf v1.30.0 + github.com/urfave/cli/v2 v2.25.0 // @grafana/backend-platform + github.com/vectordotdev/go-datemath v0.1.1-0.20220323213446-f3954d0b18ae // @grafana/backend-platform + github.com/yalue/merged_fs v1.2.2 // @grafana/grafana-as-code + github.com/yudai/gojsondiff v1.0.0 // @grafana/backend-platform + go.opentelemetry.io/collector v0.31.0 // @grafana/backend-platform + go.opentelemetry.io/collector/model v0.31.0 // @grafana/backend-platform + go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.37.0 // @grafana/grafana-operator-experience-squad + go.opentelemetry.io/otel v1.14.0 // @grafana/alerting-squad-backend + go.opentelemetry.io/otel/exporters/jaeger v1.0.0 // @grafana/backend-platform + go.opentelemetry.io/otel/sdk v1.14.0 // @grafana/backend-platform + go.opentelemetry.io/otel/trace v1.14.0 // @grafana/backend-platform + golang.org/x/crypto v0.11.0 // @grafana/backend-platform + golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // @grafana/alerting-squad-backend + golang.org/x/net v0.10.0 // @grafana/grafana-bi-squad + golang.org/x/oauth2 v0.7.0 // @grafana/grafana-authnz-team + golang.org/x/sync v0.3.0 // @grafana/alerting-squad-backend + golang.org/x/time v0.3.0 // @grafana/backend-platform + golang.org/x/tools v0.7.0 // @grafana/grafana-as-code + gonum.org/v1/gonum v0.11.0 // @grafana/observability-metrics + google.golang.org/api v0.111.0 // @grafana/backend-platform + google.golang.org/grpc v1.55.0 // @grafana/plugins-platform-backend + google.golang.org/protobuf v1.30.0 // @grafana/plugins-platform-backend gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect - gopkg.in/ini.v1 v1.67.0 - gopkg.in/mail.v2 v2.3.1 + gopkg.in/ini.v1 v1.67.0 // @grafana/alerting-squad-backend + gopkg.in/mail.v2 v2.3.1 // @grafana/backend-platform gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.1 - xorm.io/builder v0.3.6 - xorm.io/core v0.7.3 - xorm.io/xorm v0.8.2 + gopkg.in/yaml.v3 v3.0.1 // @grafana/alerting-squad-backend + xorm.io/builder v0.3.6 // indirect + xorm.io/core v0.7.3 // @grafana/backend-platform + xorm.io/xorm v0.8.2 // @grafana/alerting-squad-backend ) require ( @@ -137,7 +137,7 @@ require ( github.com/Azure/go-autorest/tracing v0.6.0 // indirect github.com/FZambia/eagle v0.0.2 // indirect github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect - github.com/andybalholm/brotli v1.0.4 + github.com/andybalholm/brotli v1.0.4 // @grafana/partner-datasources github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/beorn7/perks v1.0.1 // indirect @@ -152,31 +152,31 @@ require ( github.com/docker/go-units v0.5.0 // indirect github.com/edsrzf/mmap-go v1.1.0 // indirect github.com/emicklei/proto v1.10.0 // indirect - github.com/go-kit/log v0.2.1 + github.com/go-kit/log v0.2.1 // @grafana/backend-platform github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/go-openapi/analysis v0.21.4 // indirect github.com/go-openapi/errors v0.20.3 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect - github.com/go-openapi/loads v0.21.2 + github.com/go-openapi/loads v0.21.2 // @grafana/alerting-squad-backend github.com/go-openapi/runtime v0.25.0 // indirect github.com/go-openapi/spec v0.20.8 // indirect github.com/go-openapi/swag v0.22.3 // indirect github.com/go-openapi/validate v0.22.1 // indirect - github.com/golang-jwt/jwt/v4 v4.5.0 + github.com/golang-jwt/jwt/v4 v4.5.0 // @grafana/backend-platform github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe // indirect github.com/golang/glog v1.0.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/protobuf v1.5.3 + github.com/golang/protobuf v1.5.3 // @grafana/backend-platform github.com/google/btree v1.1.2 // indirect github.com/google/flatbuffers v2.0.8+incompatible // indirect - github.com/googleapis/gax-go/v2 v2.7.0 + github.com/googleapis/gax-go/v2 v2.7.0 // @grafana/backend-platform github.com/gorilla/mux v1.8.0 // indirect - github.com/grafana/grafana-google-sdk-go v0.1.0 + github.com/grafana/grafana-google-sdk-go v0.1.0 // @grafana/partner-datasources github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-msgpack v0.5.5 // indirect - github.com/hashicorp/go-multierror v1.1.1 + github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-sockaddr v1.0.2 // indirect github.com/hashicorp/golang-lru v0.6.0 // indirect github.com/hashicorp/yamux v0.1.1 // indirect @@ -196,7 +196,7 @@ require ( github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de // indirect github.com/oklog/run v1.1.0 // indirect github.com/oklog/ulid v1.3.1 // indirect - github.com/olekukonko/tablewriter v0.0.5 + github.com/olekukonko/tablewriter v0.0.5 // @grafana/backend-platform github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/common/sigv4 v0.1.0 // indirect github.com/prometheus/exporter-toolkit v0.9.1 // indirect @@ -215,63 +215,64 @@ require ( github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect go.mongodb.org/mongo-driver v1.11.3 // indirect go.opencensus.io v0.24.0 // indirect - go.uber.org/atomic v1.10.0 + go.uber.org/atomic v1.10.0 // @grafana/alerting-squad-backend go.uber.org/goleak v1.2.1 // indirect golang.org/x/sys v0.10.0 // indirect - golang.org/x/text v0.11.0 + golang.org/x/text v0.11.0 // @grafana/backend-platform golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 + google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // @grafana/backend-platform ) require ( - cloud.google.com/go/kms v1.4.0 - github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.0 - github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.9.0 - github.com/Azure/azure-storage-blob-go v0.15.0 - github.com/Azure/go-autorest/autorest/adal v0.9.22 - github.com/armon/go-radix v1.0.0 - github.com/blugelabs/bluge v0.1.9 - github.com/blugelabs/bluge_segment_api v0.2.0 - github.com/bufbuild/connect-go v1.4.1 - github.com/dlmiddlecote/sqlstats v1.0.2 - github.com/drone/drone-cli v1.6.1 - github.com/getkin/kin-openapi v0.115.0 - github.com/golang-migrate/migrate/v4 v4.7.0 - github.com/google/go-github/v45 v45.2.0 - github.com/grafana/codejen v0.0.3 - github.com/grafana/dskit v0.0.0-20230620150242-3dc2113b720d - github.com/grafana/phlare/api v0.1.4-0.20230426005640-f90edba05413 - github.com/huandu/xstrings v1.3.1 - github.com/jmoiron/sqlx v1.3.5 - github.com/matryer/is v1.4.0 - github.com/urfave/cli v1.22.12 - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.40.0 - go.opentelemetry.io/contrib/propagators/jaeger v1.15.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.14.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.14.0 - gocloud.dev v0.25.0 + cloud.google.com/go/kms v1.4.0 // @grafana/backend-platform + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.0 // @grafana/backend-platform + github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.9.0 // @grafana/backend-platform + github.com/Azure/azure-storage-blob-go v0.15.0 // @grafana/backend-platform + github.com/Azure/go-autorest/autorest/adal v0.9.22 // @grafana/backend-platform + github.com/armon/go-radix v1.0.0 // @grafana/grafana-app-platform-squad + github.com/blugelabs/bluge v0.1.9 // @grafana/backend-platform + github.com/blugelabs/bluge_segment_api v0.2.0 // @grafana/backend-platform + github.com/bufbuild/connect-go v1.4.1 // @grafana/observability-traces-and-profiling + github.com/dlmiddlecote/sqlstats v1.0.2 // @grafana/backend-platform + github.com/drone/drone-cli v1.6.1 // @grafana/grafana-delivery + github.com/getkin/kin-openapi v0.115.0 // @grafana/grafana-operator-experience-squad + github.com/golang-migrate/migrate/v4 v4.7.0 // @grafana/backend-platform + github.com/google/go-github/v45 v45.2.0 // @grafana/grafana-delivery + github.com/grafana/codejen v0.0.3 // @grafana/dataviz-squad + github.com/grafana/dskit v0.0.0-20230620150242-3dc2113b720d // @grafana/backend-platform + github.com/grafana/phlare/api v0.1.4-0.20230426005640-f90edba05413 // @grafana/observability-traces-and-profiling + github.com/huandu/xstrings v1.3.1 // @grafana/partner-datasources + github.com/jmoiron/sqlx v1.3.5 // @grafana/backend-platform + github.com/matryer/is v1.4.0 // @grafana/grafana-as-code + github.com/urfave/cli v1.22.12 // @grafana/backend-platform + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.40.0 // @grafana/plugins-platform-backend + go.opentelemetry.io/contrib/propagators/jaeger v1.15.0 // @grafana/backend-platform + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.14.0 // @grafana/backend-platform + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.14.0 // @grafana/backend-platform + gocloud.dev v0.25.0 // @grafana/grafana-app-platform-squad ) require ( - buf.build/gen/go/parca-dev/parca/bufbuild/connect-go v1.4.1-20221222094228-8b1d3d0f62e6.1 - buf.build/gen/go/parca-dev/parca/protocolbuffers/go v1.28.1-20221222094228-8b1d3d0f62e6.4 - github.com/Masterminds/semver/v3 v3.1.1 - github.com/alicebob/miniredis/v2 v2.30.1 - github.com/dave/dst v0.27.2 - github.com/go-jose/go-jose/v3 v3.0.0 - github.com/grafana/dataplane/examples v0.0.0-20230404174214-4d6fd58a18ad - github.com/grafana/dataplane/sdata v0.0.6 - github.com/grafana/go-mssqldb v0.9.1 - github.com/grafana/kindsys v0.0.0-20230508162304-452481b63482 - github.com/grafana/thema v0.0.0-20230615161902-b6e21996aef8 - github.com/ory/fosite v0.44.1-0.20230317114349-45a6785cc54f - github.com/redis/go-redis/v9 v9.0.2 - github.com/weaveworks/common v0.0.0-20230511094633-334485600903 - github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f - go.opentelemetry.io/contrib/samplers/jaegerremote v0.9.0 - gopkg.in/square/go-jose.v2 v2.5.2-0.20210529014059-a5c7eec3c614 - k8s.io/utils v0.0.0-20230308161112-d77c459e9343 + buf.build/gen/go/parca-dev/parca/bufbuild/connect-go v1.4.1-20221222094228-8b1d3d0f62e6.1 // @grafana/observability-traces-and-profiling + buf.build/gen/go/parca-dev/parca/protocolbuffers/go v1.28.1-20221222094228-8b1d3d0f62e6.4 // @grafana/observability-traces-and-profiling + github.com/Masterminds/semver/v3 v3.1.1 // @grafana/grafana-delivery + github.com/alicebob/miniredis/v2 v2.30.1 // @grafana/alerting-squad-backend + github.com/dave/dst v0.27.2 // @grafana/grafana-as-code + github.com/go-jose/go-jose/v3 v3.0.0 // @grafana/backend-platform + github.com/grafana/dataplane/examples v0.0.1 // @grafana/observability-metrics + github.com/grafana/dataplane/sdata v0.0.6 // @grafana/observability-metrics + github.com/grafana/go-mssqldb v0.9.1 // @grafana/grafana-bi-squad + github.com/grafana/kindsys v0.0.0-20230508162304-452481b63482 // @grafana/grafana-as-code + github.com/grafana/thema v0.0.0-20230712153715-375c1b45f3ed // @grafana/grafana-as-code + github.com/ory/fosite v0.44.1-0.20230317114349-45a6785cc54f // @grafana/grafana-authnz-team + github.com/redis/go-redis/v9 v9.0.2 // @grafana/alerting-squad-backend + github.com/weaveworks/common v0.0.0-20230511094633-334485600903 // @grafana/alerting-squad-backend + github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // @grafana/grafana-as-code + go.opentelemetry.io/contrib/samplers/jaegerremote v0.9.0 // @grafana/backend-platform + golang.org/x/mod v0.9.0 // @grafana/backend-platform + gopkg.in/square/go-jose.v2 v2.5.2-0.20210529014059-a5c7eec3c614 // @grafana/grafana-authnz-team + k8s.io/utils v0.0.0-20230308161112-d77c459e9343 // @grafana/partner-datasources ) require ( @@ -368,13 +369,13 @@ require ( require ( cloud.google.com/go/compute v1.18.0 // indirect cloud.google.com/go/iam v0.8.0 // indirect - filippo.io/age v1.1.1 + filippo.io/age v1.1.1 // @grafana/grafana-authnz-team github.com/Azure/azure-sdk-for-go/sdk/azcore v1.2.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.0 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v0.7.0 // indirect - github.com/Masterminds/sprig/v3 v3.2.2 + github.com/Masterminds/sprig/v3 v3.2.2 // @grafana/backend-platform github.com/Microsoft/go-winio v0.6.0 // indirect - github.com/ProtonMail/go-crypto v0.0.0-20230426101702-58e86b294756 + github.com/ProtonMail/go-crypto v0.0.0-20230426101702-58e86b294756 // @grafana/plugins-platform-backend github.com/RoaringBitmap/roaring v0.9.4 // indirect github.com/acomagu/bufpipe v1.0.3 // indirect github.com/axiomhq/hyperloglog v0.0.0-20191112132149-a4c4c47bc57f // indirect @@ -388,7 +389,7 @@ require ( github.com/caio/go-tdigest v3.1.0+incompatible // indirect github.com/chromedp/cdproto v0.0.0-20220208224320-6efb837e6bc2 // indirect github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 // indirect - github.com/docker/docker v23.0.4+incompatible + github.com/docker/docker v23.0.4+incompatible // @grafana/grafana-delivery github.com/elazarl/goproxy v0.0.0-20220115173737-adb46da277ac // indirect github.com/emirpasic/gods v1.12.0 // indirect github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 // indirect @@ -396,9 +397,9 @@ require ( github.com/go-git/go-billy/v5 v5.3.1 // indirect github.com/go-logr/logr v1.2.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/google/go-github v17.0.0+incompatible + github.com/google/go-github v17.0.0+incompatible // @grafana/grafana-app-platform-squad github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2 // indirect - github.com/hmarr/codeowners v1.1.2 + github.com/hmarr/codeowners v1.1.2 // @grafana/grafana-as-code github.com/imdario/mergo v0.3.13 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect @@ -411,12 +412,11 @@ require ( github.com/mschoch/smat v0.2.0 // indirect github.com/pierrec/lz4/v4 v4.1.15 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect - github.com/wk8/go-ordered-map v1.0.0 + github.com/wk8/go-ordered-map v1.0.0 // @grafana/backend-platform github.com/xanzy/ssh-agent v0.3.0 // indirect - github.com/xlab/treeprint v1.1.0 + github.com/xlab/treeprint v1.1.0 // @grafana/observability-traces-and-profiling go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.14.0 // indirect go.opentelemetry.io/proto/otlp v0.19.0 // indirect - golang.org/x/mod v0.9.0 gopkg.in/warnings.v0 v0.1.2 // indirect ) diff --git a/go.sum b/go.sum index eadef68c59a..e7a36d7c627 100644 --- a/go.sum +++ b/go.sum @@ -1360,10 +1360,12 @@ github.com/grafana/alerting v0.0.0-20230606080147-55b8d71c7890 h1:ubNIgVGX4PQ9YI github.com/grafana/alerting v0.0.0-20230606080147-55b8d71c7890/go.mod h1:zEflOvMVchYhRbFb5ziXVR/JG67FOLBzQTjhHh9xaI4= github.com/grafana/codejen v0.0.3 h1:tAWxoTUuhgmEqxJPOLtJoxlPBbMULFwKFOcRsPRPXDw= github.com/grafana/codejen v0.0.3/go.mod h1:zmwwM/DRyQB7pfuBjTWII3CWtxcXh8LTwAYGfDfpR6s= -github.com/grafana/cuetsy v0.1.9 h1:EwT8BqHoC0B3B4Y6Lg/D1aeYUKnQC1+2jjOHNsOuUfU= -github.com/grafana/cuetsy v0.1.9/go.mod h1:Ix97+CPD8ws9oSSxR3/Lf4ahU1I4Np83kjJmDVnLZvc= +github.com/grafana/cuetsy v0.1.10 h1:+W9/7roI8LorL+D1RJhKGdhsTZ81adrK9dHS0r7qsXs= +github.com/grafana/cuetsy v0.1.10/go.mod h1:Ix97+CPD8ws9oSSxR3/Lf4ahU1I4Np83kjJmDVnLZvc= github.com/grafana/dataplane/examples v0.0.0-20230404174214-4d6fd58a18ad h1:bROIOQEkRlxAt+Epn9Pos8ytkVqoIAvnaUZOAb7EgEU= github.com/grafana/dataplane/examples v0.0.0-20230404174214-4d6fd58a18ad/go.mod h1:h5YwY8s407/17XF5/dS8XrUtsTVV2RnuW8+m1Mp46mg= +github.com/grafana/dataplane/examples v0.0.1 h1:K9M5glueWyLoL4//H+EtTQq16lXuHLmOhb6DjSCahzA= +github.com/grafana/dataplane/examples v0.0.1/go.mod h1:h5YwY8s407/17XF5/dS8XrUtsTVV2RnuW8+m1Mp46mg= github.com/grafana/dataplane/sdata v0.0.6 h1:Ejlj8d1Hvy/uDLeI4sOvL34Y8WLlVDd9iN270F+8aTw= github.com/grafana/dataplane/sdata v0.0.6/go.mod h1:Jvs5ddpGmn6vcxT7tCTWAZ1mgi4sbcdFt9utQx5uMAU= github.com/grafana/dskit v0.0.0-20230620150242-3dc2113b720d h1:1w9c1/z4JypJUatX+rPIiK3eTAV7+OF094UkDIPDTIs= @@ -1394,8 +1396,8 @@ github.com/grafana/saml v0.4.13-0.20230331080031-67cbfa09c7b6 h1:oHn/OOUkECNX06D github.com/grafana/saml v0.4.13-0.20230331080031-67cbfa09c7b6/go.mod h1:igEejV+fihTIlHXYP8zOec3V5A8y3lws5bQBFsTm4gA= github.com/grafana/sqlds/v2 v2.3.10 h1:HWKhE0vR6LoEiE+Is8CSZOgaB//D1yqb2ntkass9Fd4= github.com/grafana/sqlds/v2 v2.3.10/go.mod h1:c6ibxnxRVGxV/0YkEgvy7QpQH/lyifFyV7K/14xvdIs= -github.com/grafana/thema v0.0.0-20230615161902-b6e21996aef8 h1:ESVwKjvGWVucGY65rJq0Hny3a6p8UYqEbClqXhP2qJk= -github.com/grafana/thema v0.0.0-20230615161902-b6e21996aef8/go.mod h1:KWAKeFXxQYiJ/kBVbijBLRVq9atxkfkeeFIvmj4clEA= +github.com/grafana/thema v0.0.0-20230712153715-375c1b45f3ed h1:TMtHc+B0SSNw2in6Ro1dAiBYSPRp4NzKgndFDfupt18= +github.com/grafana/thema v0.0.0-20230712153715-375c1b45f3ed/go.mod h1:3zLJnssFRPCnebCBRlq53t5LgYv9P1mbj0XMozZMTww= github.com/grafana/xorm v0.8.3-0.20220614223926-2fcda7565af6 h1:I9dh1MXGX0wGyxdV/Sl7+ugnki4Dfsy8lv2s5Yf887o= github.com/grafana/xorm v0.8.3-0.20220614223926-2fcda7565af6/go.mod h1:ZkJLEYLoVyg7amJK/5r779bHyzs2AU8f8VMiP6BM7uY= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= @@ -2247,8 +2249,8 @@ github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdh github.com/scaleway/scaleway-sdk-go v1.0.0-beta.14 h1:yFl3jyaSVLNYXlnNYM5z2pagEk1dYQhfr1p20T1NyKY= github.com/scaleway/scaleway-sdk-go v1.0.0-beta.14/go.mod h1:fCa7OJZ/9DRTnOKmxvT6pn+LPWUptQAmHF/SBJUGEcg= github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= -github.com/sdboyer/cue v0.5.0-beta.2.0.20230419165817-251c3ae823d8 h1:R7AphSCwo1rqpRCftz9wn2azIGAtdgn1AjxTiSdFJ1o= -github.com/sdboyer/cue v0.5.0-beta.2.0.20230419165817-251c3ae823d8/go.mod h1:okjJBHFQFer+a41sAe2SaGm1glWS8oEb6CmJvn5Zdws= +github.com/sdboyer/cue v0.5.0-beta.2.0.20230712135403-bdc4772ae055 h1:ilCOPMIGucstzXKgCVzAT8g6tu+Ejyk0J1HhXZN2xm0= +github.com/sdboyer/cue v0.5.0-beta.2.0.20230712135403-bdc4772ae055/go.mod h1:okjJBHFQFer+a41sAe2SaGm1glWS8oEb6CmJvn5Zdws= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/seatgeek/logrus-gelf-formatter v0.0.0-20210219220335-367fa274be2c/go.mod h1:/THDZYi7F/BsVEcYzYPqdcWFQ+1C2InkawTKfLOAnzg= diff --git a/package.json b/package.json index 62f5fe5f53a..d4327aae5aa 100644 --- a/package.json +++ b/package.json @@ -100,9 +100,9 @@ "@betterer/cli": "5.4.0", "@betterer/eslint": "5.4.0", "@betterer/regexp": "5.4.0", - "@emotion/eslint-plugin": "11.10.0", + "@emotion/eslint-plugin": "11.11.0", "@grafana/e2e": "workspace:*", - "@grafana/eslint-config": "5.1.0", + "@grafana/eslint-config": "6.0.0", "@grafana/eslint-plugin": "link:./packages/grafana-eslint-rules", "@grafana/toolkit": "workspace:*", "@grafana/tsconfig": "^1.3.0-rc1", @@ -146,9 +146,9 @@ "@types/papaparse": "5.3.7", "@types/pluralize": "^0.0.29", "@types/prismjs": "1.26.0", - "@types/react": "18.0.28", + "@types/react": "18.2.15", "@types/react-beautiful-dnd": "13.1.3", - "@types/react-dom": "18.0.11", + "@types/react-dom": "18.2.7", "@types/react-grid-layout": "1.3.2", "@types/react-highlight-words": "0.16.4", "@types/react-redux": "7.1.25", @@ -181,7 +181,7 @@ "codeowners": "^5.1.1", "copy-webpack-plugin": "11.0.0", "css-loader": "6.8.1", - "css-minimizer-webpack-plugin": "4.2.2", + "css-minimizer-webpack-plugin": "5.0.1", "cypress": "9.5.1", "esbuild": "0.18.6", "esbuild-loader": "3.0.1", @@ -209,7 +209,7 @@ "jest-date-mock": "1.0.8", "jest-environment-jsdom": "29.3.1", "jest-fail-on-console": "3.1.1", - "jest-junit": "15.0.0", + "jest-junit": "16.0.0", "jest-matcher-utils": "29.3.1", "lerna": "5.5.4", "lint-staged": "13.2.3", @@ -229,7 +229,7 @@ "react-test-renderer": "18.2.0", "redux-mock-store": "1.5.4", "rimraf": "5.0.1", - "rudder-sdk-js": "2.25.0", + "rudder-sdk-js": "2.37.0", "sass": "1.63.6", "sass-loader": "13.3.2", "sinon": "15.2.0", @@ -254,8 +254,8 @@ }, "dependencies": { "@daybrush/utils": "1.13.0", - "@emotion/css": "11.10.6", - "@emotion/react": "11.10.6", + "@emotion/css": "11.11.2", + "@emotion/react": "11.11.1", "@glideapps/glide-data-grid": "^5.2.1", "@grafana/aws-sdk": "0.0.44", "@grafana/data": "workspace:*", @@ -300,12 +300,12 @@ "@types/react-resizable": "3.0.3", "@types/trusted-types": "2.0.3", "@types/webpack-env": "1.18.0", - "@visx/event": "3.0.1", - "@visx/gradient": "3.0.0", - "@visx/group": "3.0.0", - "@visx/scale": "3.0.0", - "@visx/shape": "3.0.0", - "@visx/tooltip": "3.1.2", + "@visx/event": "3.3.0", + "@visx/gradient": "3.3.0", + "@visx/group": "3.3.0", + "@visx/scale": "3.3.0", + "@visx/shape": "3.3.0", + "@visx/tooltip": "3.3.0", "@welldone-software/why-did-you-render": "7.0.1", "angular": "1.8.3", "angular-bindonce": "0.3.1", @@ -316,7 +316,7 @@ "baron": "3.0.3", "brace": "0.11.1", "calculate-size": "1.1.1", - "centrifuge": "3.1.0", + "centrifuge": "4.0.1", "classnames": "2.3.2", "combokeys": "^3.0.0", "comlink": "4.4.1", @@ -337,7 +337,8 @@ "history": "4.10.1", "hoist-non-react-statics": "3.3.2", "i18next": "^22.0.0", - "immer": "9.0.19", + "i18next-browser-languagedetector": "^7.0.2", + "immer": "10.0.2", "immutable": "4.3.0", "jquery": "3.7.0", "js-yaml": "^4.1.0", @@ -347,7 +348,7 @@ "kbar": "0.1.0-beta.40", "lodash": "4.17.21", "logfmt": "^1.3.2", - "lru-cache": "7.17.0", + "lru-cache": "10.0.0", "lru-memoize": "^1.1.0", "marked": "^4.3.0", "memoize-one": "6.0.0", @@ -408,7 +409,7 @@ "reselect": "4.1.7", "rxjs": "7.8.0", "sass": "link:./public/sass", - "selecto": "1.22.0", + "selecto": "1.26.0", "semver": "7.5.4", "slate": "0.47.9", "slate-plain-serializer": "0.7.13", @@ -433,7 +434,7 @@ "@rushstack/rig-package": "0.4.0", "@rushstack/ts-command-line": "4.15.1", "ngtemplate-loader/loader-utils": "^2.0.0", - "trim": "0.0.3", + "trim": "1.0.1", "slate-dev-environment@^0.2.2": "patch:slate-dev-environment@npm:0.2.5#.yarn/patches/slate-dev-environment-npm-0.2.5-9aeb7da7b5.patch", "react-split-pane@0.1.92": "patch:react-split-pane@npm:0.1.92#.yarn/patches/react-split-pane-npm-0.1.92-93dbf51dff.patch", "@storybook/blocks@7.0.10": "patch:@storybook/blocks@npm%3A7.0.10#./.yarn/patches/@storybook-blocks-npm-7.0.10-b101e8dcba.patch" diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index df0173b7fa4..f9afc4726d3 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -62,8 +62,8 @@ }, "devDependencies": { "@grafana/tsconfig": "^1.2.0-rc1", - "@rollup/plugin-commonjs": "23.0.2", - "@rollup/plugin-json": "5.0.1", + "@rollup/plugin-commonjs": "25.0.2", + "@rollup/plugin-json": "6.0.0", "@rollup/plugin-node-resolve": "15.1.0", "@testing-library/dom": "9.3.0", "@testing-library/jest-dom": "5.16.5", @@ -77,8 +77,8 @@ "@types/marked": "4.0.8", "@types/node": "18.16.16", "@types/papaparse": "5.3.7", - "@types/react": "18.0.28", - "@types/react-dom": "18.0.11", + "@types/react": "18.2.15", + "@types/react-dom": "18.2.7", "@types/sinon": "10.0.13", "@types/testing-library__jest-dom": "5.14.5", "@types/tinycolor2": "1.4.3", diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index c7bb1c3e7d4..5012f55301a 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -113,4 +113,5 @@ export interface FeatureToggles { logsExploreTableVisualisation?: boolean; awsDatasourcesTempCredentials?: boolean; transformationsRedesign?: boolean; + mlExpressions?: boolean; } diff --git a/packages/grafana-e2e-selectors/package.json b/packages/grafana-e2e-selectors/package.json index 723e29fd2ac..175f65417fb 100644 --- a/packages/grafana-e2e-selectors/package.json +++ b/packages/grafana-e2e-selectors/package.json @@ -39,7 +39,7 @@ "postpack": "mv package.json.bak package.json" }, "devDependencies": { - "@rollup/plugin-commonjs": "23.0.2", + "@rollup/plugin-commonjs": "25.0.2", "@rollup/plugin-node-resolve": "15.1.0", "@types/node": "18.16.16", "esbuild": "0.18.6", diff --git a/packages/grafana-runtime/package.json b/packages/grafana-runtime/package.json index 7c54ef32bee..4dce798898f 100644 --- a/packages/grafana-runtime/package.json +++ b/packages/grafana-runtime/package.json @@ -49,7 +49,7 @@ }, "devDependencies": { "@grafana/tsconfig": "^1.2.0-rc1", - "@rollup/plugin-commonjs": "23.0.2", + "@rollup/plugin-commonjs": "25.0.2", "@rollup/plugin-node-resolve": "15.1.0", "@testing-library/dom": "9.3.0", "@testing-library/react": "14.0.0", @@ -58,8 +58,8 @@ "@types/history": "4.7.11", "@types/jest": "29.2.3", "@types/lodash": "4.14.191", - "@types/react": "18.0.28", - "@types/react-dom": "18.0.11", + "@types/react": "18.2.15", + "@types/react-dom": "18.2.7", "@types/systemjs": "^0.20.6", "esbuild": "0.18.6", "lodash": "4.17.21", diff --git a/packages/grafana-schema/package.json b/packages/grafana-schema/package.json index f48a0fe1759..f1ce5354c27 100644 --- a/packages/grafana-schema/package.json +++ b/packages/grafana-schema/package.json @@ -37,8 +37,8 @@ }, "devDependencies": { "@grafana/tsconfig": "^1.2.0-rc1", - "@rollup/plugin-commonjs": "23.0.2", - "@rollup/plugin-json": "5.0.1", + "@rollup/plugin-commonjs": "25.0.2", + "@rollup/plugin-json": "6.0.0", "@rollup/plugin-node-resolve": "15.1.0", "esbuild": "0.18.6", "glob": "^10.2.7", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 64c838ffe1e..f1f602b427e 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -47,8 +47,8 @@ "not IE 11" ], "dependencies": { - "@emotion/css": "11.10.6", - "@emotion/react": "11.10.6", + "@emotion/css": "11.11.2", + "@emotion/react": "11.11.1", "@grafana/data": "10.1.0-pre", "@grafana/e2e-selectors": "10.1.0-pre", "@grafana/faro-web-sdk": "1.1.0", @@ -71,6 +71,7 @@ "date-fns": "2.30.0", "hoist-non-react-statics": "3.3.2", "i18next": "^22.0.0", + "i18next-browser-languagedetector": "^7.0.2", "immutable": "4.3.0", "is-hotkey": "0.2.0", "jquery": "3.7.0", @@ -84,7 +85,7 @@ "rc-drawer": "6.3.0", "rc-slider": "10.2.1", "rc-time-picker": "^3.7.3", - "rc-tooltip": "5.3.1", + "rc-tooltip": "6.0.1", "react-beautiful-dnd": "13.1.1", "react-calendar": "4.0.0", "react-colorful": "5.6.1", @@ -147,11 +148,11 @@ "@types/mock-raf": "1.0.3", "@types/node": "18.16.16", "@types/prismjs": "1.26.0", - "@types/react": "18.0.28", + "@types/react": "18.2.15", "@types/react-beautiful-dnd": "13.1.3", "@types/react-calendar": "3.9.0", "@types/react-color": "3.0.6", - "@types/react-dom": "18.0.11", + "@types/react-dom": "18.2.7", "@types/react-highlight-words": "0.16.4", "@types/react-router-dom": "5.3.3", "@types/react-table": "7.7.14", diff --git a/packages/grafana-ui/src/components/Alert/Alert.tsx b/packages/grafana-ui/src/components/Alert/Alert.tsx index e13dd28a294..42ccd327722 100644 --- a/packages/grafana-ui/src/components/Alert/Alert.tsx +++ b/packages/grafana-ui/src/components/Alert/Alert.tsx @@ -101,6 +101,7 @@ Alert.displayName = 'Alert'; export const getIconFromSeverity = (severity: AlertVariant): IconName => { switch (severity) { case 'error': + return 'exclamation-circle'; case 'warning': return 'exclamation-triangle'; case 'info': diff --git a/packages/grafana-ui/src/components/Dropdown/ButtonSelect.tsx b/packages/grafana-ui/src/components/Dropdown/ButtonSelect.tsx index 131b00e920e..1cbd25e54ec 100644 --- a/packages/grafana-ui/src/components/Dropdown/ButtonSelect.tsx +++ b/packages/grafana-ui/src/components/Dropdown/ButtonSelect.tsx @@ -64,7 +64,7 @@ const ButtonSelectComponent = (props: Props) => { tabIndex=-1 is needed here to support highlighting text within the menu when using FocusScope see https://github.com/adobe/react-spectrum/issues/1604#issuecomment-781574668 */} - + {options.map((item) => ( React.ReactNode; prepConfig: (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => UPlotConfigBuilder; propsToDiff?: Array; - preparePlotFrame?: (frames: DataFrame[], dimFields: XYFieldMatchers) => DataFrame; + preparePlotFrame?: (frames: DataFrame[], dimFields: XYFieldMatchers) => DataFrame | null; renderLegend: (config: UPlotConfigBuilder) => React.ReactElement | null; /** diff --git a/packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.ts b/packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.ts index 9d58f0cde29..352f2f0660b 100644 --- a/packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.ts +++ b/packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.ts @@ -1,4 +1,6 @@ -import { DataFrame, FieldType } from '@grafana/data'; +import { DataFrame } from '@grafana/data'; + +import { getRefField } from './utils'; type InsertMode = (prev: number, next: number, threshold: number) => number; @@ -29,10 +31,7 @@ export function applyNullInsertThreshold(opts: NullInsertOptions): DataFrame { insertMode = INSERT_MODES.threshold; } - const refField = frame.fields.find((field) => { - // note: getFieldDisplayName() would require full DF[] - return refFieldName != null ? field.name === refFieldName : field.type === FieldType.time; - }); + const refField = getRefField(frame, refFieldName); if (refField == null) { return frame; diff --git a/packages/grafana-ui/src/components/GraphNG/utils.ts b/packages/grafana-ui/src/components/GraphNG/utils.ts index 3f46c2d9dbe..cfdc8e36456 100644 --- a/packages/grafana-ui/src/components/GraphNG/utils.ts +++ b/packages/grafana-ui/src/components/GraphNG/utils.ts @@ -18,9 +18,17 @@ function isVisibleBarField(f: Field) { ); } +export function getRefField(frame: DataFrame, refFieldName?: string | null) { + return frame.fields.find((field) => { + // note: getFieldDisplayName() would require full DF[] + return refFieldName != null ? field.name === refFieldName : field.type === FieldType.time; + }); +} + // will mutate the DataFrame's fields' values -function applySpanNullsThresholds(frame: DataFrame) { - let refField = frame.fields.find((field) => field.type === FieldType.time); // this doesnt need to be time, just any numeric/asc join field +function applySpanNullsThresholds(frame: DataFrame, refFieldName?: string | null) { + const refField = getRefField(frame, refFieldName); + let refValues = refField?.values as any[]; for (let i = 0; i < frame.fields.length; i++) { @@ -43,12 +51,22 @@ function applySpanNullsThresholds(frame: DataFrame) { } export function preparePlotFrame(frames: DataFrame[], dimFields: XYFieldMatchers, timeRange?: TimeRange | null) { + let xField: Field; + loop: for (let frame of frames) { + for (let field of frame.fields) { + if (dimFields.x(field, frame, frames)) { + xField = field; + break loop; + } + } + } + // apply null insertions at interval frames = frames.map((frame) => { - if (!frame.fields[0].state?.nullThresholdApplied) { + if (!xField?.state?.nullThresholdApplied) { return applyNullInsertThreshold({ frame, - refFieldName: null, + refFieldName: xField.name, refFieldPseudoMin: timeRange?.from.valueOf(), refFieldPseudoMax: timeRange?.to.valueOf(), }); @@ -84,7 +102,7 @@ export function preparePlotFrame(frames: DataFrame[], dimFields: XYFieldMatchers return; } - const xVals = frame.fields[0].values; + const xVals = xField.values; for (let i = 0; i < xVals.length; i++) { if (i > 0) { @@ -102,7 +120,7 @@ export function preparePlotFrame(frames: DataFrame[], dimFields: XYFieldMatchers }); if (alignedFrame) { - alignedFrame = applySpanNullsThresholds(alignedFrame); + alignedFrame = applySpanNullsThresholds(alignedFrame, xField!.name); // append 2 null vals at minXDelta to bar series if (minXDelta !== Infinity) { diff --git a/packages/grafana-ui/src/components/Link/TextLink.mdx b/packages/grafana-ui/src/components/Link/TextLink.mdx new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/grafana-ui/src/components/Link/TextLink.story.tsx b/packages/grafana-ui/src/components/Link/TextLink.story.tsx new file mode 100644 index 00000000000..8ef60faa363 --- /dev/null +++ b/packages/grafana-ui/src/components/Link/TextLink.story.tsx @@ -0,0 +1,80 @@ +import { Meta, StoryFn } from '@storybook/react'; +import React from 'react'; + +import { StoryExample } from '../../utils/storybook/StoryExample'; +import { VerticalGroup } from '../Layout/Layout'; +import { P } from '../Text/TextElements'; + +import { TextLink } from './TextLink'; +import mdx from './TextLink.mdx'; + +const meta: Meta = { + title: 'General/TextLink', + component: TextLink, + parameters: { + docs: { + page: mdx, + }, + controls: { exclude: ['href', 'external'] }, + }, + argTypes: { + variant: { control: 'select', options: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'body', 'bodySmall', undefined] }, + weight: { + control: 'select', + options: ['bold', 'medium', 'light', 'regular', undefined], + }, + color: { + control: 'select', + options: ['primary', 'secondary', 'disabled', 'link', 'maxContrast', undefined], + }, + inline: { control: 'boolean' }, + }, + args: { + variant: 'body', + weight: 'light', + color: undefined, + inline: false, + href: 'https://www.google.com', + external: true, + icon: 'external-link-alt', + }, +}; + +export const Example: StoryFn = (args) => { + return ( + + +

+ To get started with a forever free Grafana Cloud account, sign up at   + + grafana.com + + . +

+
+ + + Learn how in the docs + + +
+

*The examples cannot contemplate an internal link due to conflicts between Storybook and React Router

+
+ ); +}; + +Example.parameters = { + controls: { exclude: ['href', 'external', 'variant', 'weight', 'color', 'inline', 'icon'] }, +}; + +export const Basic: StoryFn = (args) => { + return ( +
+ + Go to Google + +
+ ); +}; + +export default meta; diff --git a/packages/grafana-ui/src/components/Link/TextLink.tsx b/packages/grafana-ui/src/components/Link/TextLink.tsx new file mode 100644 index 00000000000..2398e5dd11c --- /dev/null +++ b/packages/grafana-ui/src/components/Link/TextLink.tsx @@ -0,0 +1,105 @@ +import { css } from '@emotion/css'; +import React, { AnchorHTMLAttributes, forwardRef } from 'react'; + +import { GrafanaTheme2, locationUtil, textUtil, ThemeTypographyVariantTypes } from '@grafana/data'; + +import { useTheme2 } from '../../themes'; +import { IconName, IconSize } from '../../types'; +import { Icon } from '../Icon/Icon'; +import { customWeight } from '../Text/utils'; + +import { Link } from './Link'; + +interface TextLinkProps extends Omit, 'target' | 'rel'> { + /** url to which redirect the user, external or internal */ + href: string; + /** Color to use for text */ + color?: keyof GrafanaTheme2['colors']['text']; + /** Specify if the link will redirect users to a page in or out Grafana */ + external?: boolean; + /** True when the link will be displayed inline with surrounding text, false if it will be displayed as a block. Depending on this prop correspondant default styles will be applied */ + inline?: boolean; + /** The default variant is 'body'. To fit another styles set the correspondent variant as it is necessary also to adjust the icon size */ + variant?: keyof ThemeTypographyVariantTypes; + /** Override the default weight for the used variant */ + weight?: 'light' | 'regular' | 'medium' | 'bold'; + /** Set the icon to be shown. An external link will show the 'external-link-alt' icon as default.*/ + icon?: IconName; + children: string; +} + +const svgSizes: { + [key in keyof ThemeTypographyVariantTypes]: IconSize; +} = { + h1: 'xl', + h2: 'xl', + h3: 'lg', + h4: 'lg', + h5: 'md', + h6: 'md', + body: 'md', + bodySmall: 'xs', +}; + +export const TextLink = forwardRef( + ( + { href, color = 'link', external = false, inline = true, variant = 'body', weight, icon, children, ...rest }, + ref + ) => { + const validUrl = locationUtil.stripBaseFromUrl(textUtil.sanitizeUrl(href ?? '')); + + const theme = useTheme2(); + const styles = getLinkStyles(theme, inline, variant, weight, color); + const externalIcon = icon || 'external-link-alt'; + + return external ? ( + + {children} + + + ) : ( + + {children} + {icon && } + + ); + } +); + +TextLink.displayName = 'TextLink'; + +export const getLinkStyles = ( + theme: GrafanaTheme2, + inline: boolean, + variant?: keyof ThemeTypographyVariantTypes, + weight?: TextLinkProps['weight'], + color?: TextLinkProps['color'] +) => { + return css([ + variant && { + ...theme.typography[variant], + }, + weight && { + fontWeight: customWeight(weight, theme), + }, + color && { + color: theme.colors.text[color], + }, + { + alignItems: 'center', + gap: '0.25em', + display: 'inline-flex', + textDecoration: 'none', + '&:hover': { + textDecoration: 'underline', + color: theme.colors.text.link, + }, + }, + inline && { + textDecoration: 'underline', + '&:hover': { + textDecoration: 'none', + }, + }, + ]); +}; diff --git a/packages/grafana-ui/src/components/Slider/HandleTooltip.tsx b/packages/grafana-ui/src/components/Slider/HandleTooltip.tsx index 034f01348b1..bd195649b08 100644 --- a/packages/grafana-ui/src/components/Slider/HandleTooltip.tsx +++ b/packages/grafana-ui/src/components/Slider/HandleTooltip.tsx @@ -6,10 +6,10 @@ import { GrafanaTheme2 } from '@grafana/data'; import { useStyles2 } from '../../themes/ThemeContext'; +// this is now typed in rc-tooltip, but they don't export it :( +// let's mirror the interface here. if there's any discrepancy, we'll get a type error interface RCTooltipRef { - // rc-tooltip's ref is essentially untyped, so we be cautious by saying the function is - // potentially undefined which, given rc's track record, seems likely :) - forcePopupAlign?: () => {}; + forceAlign: () => {}; } const HandleTooltip = (props: { @@ -21,7 +21,7 @@ const HandleTooltip = (props: { }) => { const { value, children, visible, placement, tipFormatter, ...restProps } = props; - const tooltipRef = useRef(); + const tooltipRef = useRef(null); const rafRef = useRef(null); const styles = useStyles2(tooltipStyles); @@ -33,7 +33,7 @@ const HandleTooltip = (props: { function keepAlign() { rafRef.current = requestAnimationFrame(() => { - tooltipRef.current?.forcePopupAlign?.(); + tooltipRef.current?.forceAlign(); }); } diff --git a/packages/grafana-ui/src/components/Text/Text.tsx b/packages/grafana-ui/src/components/Text/Text.tsx index fa191317f90..d8e7d460c4f 100644 --- a/packages/grafana-ui/src/components/Text/Text.tsx +++ b/packages/grafana-ui/src/components/Text/Text.tsx @@ -5,6 +5,8 @@ import { GrafanaTheme2, ThemeTypographyVariantTypes } from '@grafana/data'; import { useStyles2 } from '../../themes'; +import { customWeight, customColor } from './utils'; + export interface TextProps { /** Defines what HTML element is defined underneath */ as: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'span' | 'p' | 'legend'; @@ -75,32 +77,3 @@ const getTextStyles = ( }, ]); }; - -const customWeight = (weight: TextProps['weight'], theme: GrafanaTheme2): number => { - switch (weight) { - case 'bold': - return theme.typography.fontWeightBold; - case 'medium': - return theme.typography.fontWeightMedium; - case 'light': - return theme.typography.fontWeightLight; - case 'regular': - case undefined: - return theme.typography.fontWeightRegular; - } -}; - -const customColor = (color: TextProps['color'], theme: GrafanaTheme2): string | undefined => { - switch (color) { - case 'error': - return theme.colors.error.text; - case 'success': - return theme.colors.success.text; - case 'info': - return theme.colors.info.text; - case 'warning': - return theme.colors.warning.text; - default: - return color ? theme.colors.text[color] : undefined; - } -}; diff --git a/packages/grafana-ui/src/components/Text/utils.ts b/packages/grafana-ui/src/components/Text/utils.ts new file mode 100644 index 00000000000..650e6ecae74 --- /dev/null +++ b/packages/grafana-ui/src/components/Text/utils.ts @@ -0,0 +1,32 @@ +import { GrafanaTheme2 } from '@grafana/data'; + +import { TextProps } from './Text'; + +export const customWeight = (weight: TextProps['weight'], theme: GrafanaTheme2): number => { + switch (weight) { + case 'bold': + return theme.typography.fontWeightBold; + case 'medium': + return theme.typography.fontWeightMedium; + case 'light': + return theme.typography.fontWeightLight; + case 'regular': + case undefined: + return theme.typography.fontWeightRegular; + } +}; + +export const customColor = (color: TextProps['color'], theme: GrafanaTheme2): string | undefined => { + switch (color) { + case 'error': + return theme.colors.error.text; + case 'success': + return theme.colors.success.text; + case 'info': + return theme.colors.info.text; + case 'warning': + return theme.colors.warning.text; + default: + return color ? theme.colors.text[color] : undefined; + } +}; diff --git a/packages/grafana-ui/src/components/Tooltip/Popover.tsx b/packages/grafana-ui/src/components/Tooltip/Popover.tsx index fa9b3d3d643..ea11c2f7aef 100644 --- a/packages/grafana-ui/src/components/Tooltip/Popover.tsx +++ b/packages/grafana-ui/src/components/Tooltip/Popover.tsx @@ -23,7 +23,7 @@ const transitionStyles: { [key: string]: object } = { export type RenderPopperArrowFn = (props: { arrowProps: PopperArrowProps; placement: string }) => JSX.Element; -interface Props extends React.HTMLAttributes { +interface Props extends Omit, 'content'> { show: boolean; placement?: Placement; content: PopoverContent; diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 65ca761d285..98969e5e40e 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -204,6 +204,7 @@ export { ToolbarButton, ToolbarButtonRow } from './ToolbarButton'; export { ValuePicker } from './ValuePicker/ValuePicker'; export { fieldMatchersUI } from './MatchersUI/fieldMatchersUI'; export { Link } from './Link/Link'; +export { TextLink } from './Link/TextLink'; export { Label } from './Forms/Label'; export { Field, type FieldProps } from './Forms/Field'; diff --git a/packages/grafana-ui/src/utils/i18n.tsx b/packages/grafana-ui/src/utils/i18n.tsx index f6777c6bad6..06cd0731653 100644 --- a/packages/grafana-ui/src/utils/i18n.tsx +++ b/packages/grafana-ui/src/utils/i18n.tsx @@ -14,7 +14,8 @@ import { Trans as I18NextTrans, initReactI18next } from 'react-i18next'; // esli // Creates a default, english i18next instance when running outside of grafana. // we don't support changing the locale of grafana ui when outside of Grafana function initI18n() { - if (!i18next.options.lng) { + // resources is undefined by default and set either by grafana app.ts or here + if (typeof i18next.options.resources !== 'object') { i18next.use(initReactI18next).init({ resources: {}, returnEmptyString: false, diff --git a/pkg/api/plugin_resource_test.go b/pkg/api/plugin_resource_test.go index b7409befb4a..97c08eaf6e0 100644 --- a/pkg/api/plugin_resource_test.go +++ b/pkg/api/plugin_resource_test.go @@ -70,7 +70,7 @@ func TestCallResource(t *testing.T) { angularInspector, err := angularinspector.NewStaticInspector() require.NoError(t, err) l := loader.ProvideService(pCfg, fakes.NewFakeLicensingService(), signature.NewUnsignedAuthorizer(pCfg), - reg, provider.ProvideService(coreRegistry), finder.NewLocalFinder(pCfg), fakes.NewFakeRoleRegistry(), + reg, provider.ProvideService(coreRegistry), finder.NewLocalFinder(pCfg.DevMode), fakes.NewFakeRoleRegistry(), assetpath.ProvideService(pluginscdn.ProvideService(pCfg)), signature.ProvideService(statickey.New()), angularInspector, &fakes.FakeOauthService{}) srcs := sources.ProvideService(cfg, pCfg) diff --git a/pkg/cmd/grafana-cli/commands/install_command.go b/pkg/cmd/grafana-cli/commands/install_command.go index a17a3769277..92969cc3335 100644 --- a/pkg/cmd/grafana-cli/commands/install_command.go +++ b/pkg/cmd/grafana-cli/commands/install_command.go @@ -5,11 +5,11 @@ import ( "errors" "fmt" "os" - "path/filepath" "runtime" "strings" "github.com/fatih/color" + "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" "github.com/grafana/grafana/pkg/cmd/grafana-cli/models" "github.com/grafana/grafana/pkg/cmd/grafana-cli/services" @@ -73,6 +73,14 @@ func installCommand(c utils.CommandLine) error { // installPlugin downloads the plugin code as a zip file from the Grafana.com API // and then extracts the zip into the plugin's directory. func installPlugin(ctx context.Context, pluginID, version string, c utils.CommandLine) error { + // If a version is specified, check if it is already installed + if version != "" { + if services.PluginVersionInstalled(pluginID, version, c.PluginDirectory()) { + services.Logger.Successf("Plugin %s v%s already installed.", pluginID, version) + return nil + } + } + repository := repo.NewManager(repo.ManagerCfg{ SkipTLSVerify: c.Bool("insecure"), BaseURL: c.PluginRepoURL(), @@ -95,7 +103,7 @@ func installPlugin(ctx context.Context, pluginID, version string, c utils.Comman } pluginFs := storage.FileSystem(services.Logger, c.PluginDirectory()) - extractedArchive, err := pluginFs.Extract(ctx, pluginID, archive.File) + extractedArchive, err := pluginFs.Extract(ctx, pluginID, storage.SimpleDirNameGeneratorFunc, archive.File) if err != nil { return err } @@ -107,7 +115,7 @@ func installPlugin(ctx context.Context, pluginID, version string, c utils.Comman return fmt.Errorf("%v: %w", fmt.Sprintf("failed to download plugin %s from repository", dep.ID), err) } - _, err = pluginFs.Extract(ctx, dep.ID, d.File) + _, err = pluginFs.Extract(ctx, dep.ID, storage.SimpleDirNameGeneratorFunc, d.File) if err != nil { return err } @@ -117,16 +125,21 @@ func installPlugin(ctx context.Context, pluginID, version string, c utils.Comman // uninstallPlugin removes the plugin directory func uninstallPlugin(_ context.Context, pluginID string, c utils.CommandLine) error { - logger.Infof("Removing plugin: %v\n", pluginID) - - pluginPath := filepath.Join(c.PluginDirectory(), pluginID) - fs := plugins.NewLocalFS(pluginPath) - - logger.Debugf("Removing directory %v\n", pluginPath) - err := fs.Remove() - if err != nil { - return err + for _, bundle := range services.GetLocalPlugins(c.PluginDirectory()) { + if bundle.Primary.JSONData.ID == pluginID { + logger.Infof("Removing plugin: %v\n", pluginID) + if remover, ok := bundle.Primary.FS.(plugins.FSRemover); ok { + logger.Debugf("Removing directory %v\n\n", bundle.Primary.FS.Base()) + if err := remover.Remove(); err != nil { + return err + } + return nil + } else { + return fmt.Errorf("plugin %v is immutable and therefore cannot be uninstalled", pluginID) + } + } } + return nil } diff --git a/pkg/cmd/grafana-cli/services/services.go b/pkg/cmd/grafana-cli/services/services.go index 46f9b4c643f..00fffc487e2 100644 --- a/pkg/cmd/grafana-cli/services/services.go +++ b/pkg/cmd/grafana-cli/services/services.go @@ -13,7 +13,6 @@ import ( "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" "github.com/grafana/grafana/pkg/cmd/grafana-cli/models" "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/manager/loader/finder" "github.com/grafana/grafana/pkg/plugins/manager/sources" ) @@ -78,7 +77,7 @@ func GetLocalPlugin(pluginDir, pluginID string) (plugins.FoundPlugin, error) { } func GetLocalPlugins(pluginDir string) []*plugins.FoundBundle { - f := finder.NewLocalFinder(&config.Cfg{}) + f := finder.NewLocalFinder(true) res, err := f.Find(context.Background(), sources.NewLocalSource(plugins.ClassExternal, []string{pluginDir})) if err != nil { @@ -88,3 +87,15 @@ func GetLocalPlugins(pluginDir string) []*plugins.FoundBundle { return res } + +func PluginVersionInstalled(pluginID, version, pluginDir string) bool { + for _, bundle := range GetLocalPlugins(pluginDir) { + pJSON := bundle.Primary.JSONData + if pJSON.ID == pluginID { + if pJSON.Info.Version == version { + return true + } + } + } + return false +} diff --git a/pkg/codegen/jenny_tsveneerindex.go b/pkg/codegen/jenny_tsveneerindex.go index b3dc445a832..06981281148 100644 --- a/pkg/codegen/jenny_tsveneerindex.go +++ b/pkg/codegen/jenny_tsveneerindex.go @@ -150,7 +150,7 @@ func (gen *genTSVeneerIndex) extractTSIndexVeneerElements(def kindsys.Kind, tf * return nil, terr } - vpath := fmt.Sprintf("v%v", thema.LatestVersion(lin)[0]) + vpath := fmt.Sprintf("v%v", thema.Lineage.Latest(lin).Version()) if def.Props().Common().Maturity.Less(kindsys.MaturityStable) { vpath = "x" } @@ -182,7 +182,7 @@ func (gen *genTSVeneerIndex) extractTSIndexVeneerElements(def kindsys.Kind, tf * // and exports all the symbols in the list. // // TODO generate code such that tsc enforces type compatibility between raw and veneer decls`, - lin.Name(), thema.LatestVersion(lin), filepath.ToSlash(filepath.Join(gen.dir, vtfile))) + lin.Name(), thema.Lineage.Latest(lin).Version(), filepath.ToSlash(filepath.Join(gen.dir, vtfile))) customComments := []ast.Comment{{Text: customstr}} if len(custom) > 0 { diff --git a/pkg/expr/graph.go b/pkg/expr/graph.go index b8b7d1bce45..eb26a87117f 100644 --- a/pkg/expr/graph.go +++ b/pkg/expr/graph.go @@ -11,6 +11,7 @@ import ( "gonum.org/v1/gonum/graph/topo" "github.com/grafana/grafana/pkg/expr/mathexp" + "github.com/grafana/grafana/pkg/services/featuremgmt" ) // NodeType is the type of a DPNode. Currently either a expression command or datasource query. @@ -21,6 +22,8 @@ const ( TypeCMDNode NodeType = iota // TypeDatasourceNode is a NodeType for datasource queries. TypeDatasourceNode + // TypeMLNode is a NodeType for Machine Learning queries. + TypeMLNode ) func (nt NodeType) String() string { @@ -29,6 +32,8 @@ func (nt NodeType) String() string { return "Expression" case TypeDatasourceNode: return "Datasource" + case TypeMLNode: + return "Machine Learning" default: return "Unknown" } @@ -159,6 +164,7 @@ func (s *Service) buildGraph(req *Request) (*simple.DirectedGraph, error) { rn := &rawNode{ Query: rawQueryProp, + QueryRaw: query.JSON, RefID: query.RefID, TimeRange: query.TimeRange, QueryType: query.QueryType, @@ -171,7 +177,16 @@ func (s *Service) buildGraph(req *Request) (*simple.DirectedGraph, error) { node, err = s.buildDSNode(dp, rn, req) case TypeCMDNode: node, err = buildCMDNode(dp, rn) - default: + case TypeMLNode: + if s.features.IsEnabled(featuremgmt.FlagMlExpressions) { + node, err = s.buildMLNode(dp, rn, req) + if err != nil { + err = fmt.Errorf("fail to parse expression with refID %v: %w", rn.RefID, err) + } + } + } + + if node == nil && err == nil { err = fmt.Errorf("unsupported node type '%s'", NodeTypeFromDatasourceUID(query.DataSource.UID)) } diff --git a/pkg/expr/ml.go b/pkg/expr/ml.go new file mode 100644 index 00000000000..902a64aef88 --- /dev/null +++ b/pkg/expr/ml.go @@ -0,0 +1,158 @@ +package expr + +import ( + "context" + "errors" + "fmt" + "net/http" + "strings" + "time" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + jsoniter "github.com/json-iterator/go" + "gonum.org/v1/gonum/graph/simple" + + "github.com/grafana/grafana/pkg/api/response" + "github.com/grafana/grafana/pkg/expr/mathexp" + "github.com/grafana/grafana/pkg/expr/ml" + "github.com/grafana/grafana/pkg/plugins/httpresponsesender" + "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" +) + +var ( + errMLPluginDoesNotExist = fmt.Errorf("expression type Machine Learning cannot be executed. Plugin '%s' must be installed and initialized", mlPluginID) +) + +const ( + // mlDatasourceID is similar to a fake ID for CMDNode. There is no specific reason for the selection of this value. + mlDatasourceID = -200 + + // DatasourceUID is the string constant used as the datasource name in requests + // to identify it as an expression command when use in Datasource.UID. + MLDatasourceUID = "__ml__" + + // mlPluginID is a known constant and used in other places of the code + mlPluginID = "grafana-ml-app" +) + +// MLNode is a node of expression tree that evaluates the expression by sending the payload to Machine Learning back-end. +// See ml.UnmarshalCommand for supported commands. +type MLNode struct { + baseNode + command ml.Command + TimeRange TimeRange + request *Request +} + +// NodeType returns the data pipeline node type. +func (m *MLNode) NodeType() NodeType { + return TypeMLNode +} + +// Execute initializes plugin API client, executes a ml.Command and then converts the result of the execution. +// Returns non-empty mathexp.Results if evaluation was successful. Returns QueryError if command execution failed +func (m *MLNode) Execute(ctx context.Context, now time.Time, _ mathexp.Vars, s *Service) (r mathexp.Results, e error) { + logger := logger.FromContext(ctx).New("datasourceType", mlPluginID, "queryRefId", m.refID) + var result mathexp.Results + timeRange := m.TimeRange.AbsoluteTime(now) + + // get the plugin configuration that will be used by client (auth, host, etc) + pCtx, err := s.pCtxProvider.Get(ctx, mlPluginID, m.request.User, m.request.OrgId) + if err != nil { + if errors.Is(err, plugincontext.ErrPluginNotFound) { + return result, errMLPluginDoesNotExist + } + return result, fmt.Errorf("failed to get plugin settings: %w", err) + } + + // Plugin must be initialized by the admin first. That will create service account, and update plugin settings so all requests can use it. + // Fail if it is not initialized. + if pCtx.AppInstanceSettings == nil || !jsoniter.Get(pCtx.AppInstanceSettings.JSONData, "initialized").ToBool() { + return mathexp.Results{}, errMLPluginDoesNotExist + } + + // responseType and respStatus will be updated below. Use defer to ensure that debug log message is always emitted + responseType := "unknown" + respStatus := "success" + defer func() { + if e != nil { + responseType = "error" + respStatus = "failure" + } + logger.Debug("Data source queried", "responseType", responseType) + useDataplane := strings.HasPrefix("dataplane-", responseType) + s.metrics.dsRequests.WithLabelValues(respStatus, fmt.Sprintf("%t", useDataplane)).Inc() + }() + + // Execute the command and provide callback function for sending a request via plugin API. + // This lets us make commands abstracted from peculiarities of the transfer protocol. + data, err := m.command.Execute(timeRange.From, timeRange.To, func(method string, path string, payload []byte) (response.Response, error) { + crReq := &backend.CallResourceRequest{ + PluginContext: pCtx, + Path: path, + Method: method, + URL: path, + Headers: make(map[string][]string, len(m.request.Headers)), + Body: payload, + } + + // copy headers from the request to evaluate the expression pipeline. Usually this contains information from upstream, e.g. FromAlert + for key, val := range m.request.Headers { + crReq.SetHTTPHeader(key, val) + } + + resp := response.CreateNormalResponse(make(http.Header), nil, 0) + httpSender := httpresponsesender.New(resp) + err = s.pluginsClient.CallResource(ctx, crReq, httpSender) + if err != nil { + return nil, err + } + return resp, nil + }) + + if err != nil { + return result, QueryError{ + RefID: m.refID, + Err: err, + } + } + + // data is not guaranteed to be specified. In this case simulate NoData scenario + if data == nil { + data = &backend.QueryDataResponse{Responses: map[string]backend.DataResponse{}} + } + + dataFrames, err := getResponseFrame(data, m.refID) + if err != nil { + return mathexp.Results{}, QueryError{ + RefID: m.refID, + DatasourceUID: mlPluginID, + Err: err, + } + } + + // process the response the same way DSNode does. Use plugin ID as data source type. Semantically, they are the same. + responseType, result, err = convertDataFramesToResults(ctx, dataFrames, mlPluginID, s, logger) + return result, err +} + +func (s *Service) buildMLNode(dp *simple.DirectedGraph, rn *rawNode, req *Request) (Node, error) { + if rn.TimeRange == nil { + return nil, errors.New("time range must be specified") + } + + cmd, err := ml.UnmarshalCommand(rn.QueryRaw, s.cfg.AppURL) + if err != nil { + return nil, err + } + + return &MLNode{ + baseNode: baseNode{ + id: dp.NewNode().ID(), + refID: rn.RefID, + }, + TimeRange: rn.TimeRange, + command: cmd, + request: req, + }, nil +} diff --git a/pkg/expr/ml/model.go b/pkg/expr/ml/model.go new file mode 100644 index 00000000000..00ae818996b --- /dev/null +++ b/pkg/expr/ml/model.go @@ -0,0 +1,83 @@ +package ml + +import ( + "strings" + "time" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + jsoniter "github.com/json-iterator/go" +) + +type CommandConfiguration struct { + Type string `json:"type"` + IntervalMs *uint `json:"intervalMs,omitempty"` + Config jsoniter.RawMessage `json:"config"` +} + +type OutlierCommandConfiguration struct { + DatasourceType string `json:"datasource_type"` + DatasourceUID string `json:"datasource_uid,omitempty"` + + // If Query is empty it should be contained in a datasource specific format + // inside of QueryParms. + Query string `json:"query,omitempty"` + QueryParams map[string]interface{} `json:"query_params,omitempty"` + + Algorithm map[string]interface{} `json:"algorithm"` + ResponseType string `json:"response_type"` +} + +// outlierAttributes is outlier command configuration that is sent to Machine learning API +type outlierAttributes struct { + OutlierCommandConfiguration + GrafanaURL string `json:"grafana_url"` + StartEndAttributes timeRangeAndInterval `json:"start_end_attributes"` +} + +type outlierData struct { + Attributes outlierAttributes `json:"attributes"` +} + +// outlierRequestBody describes a request body that is sent to Outlier API +type outlierRequestBody struct { + Data outlierData `json:"data"` +} + +type timeRangeAndInterval struct { + Start mlTime `json:"start"` + End mlTime `json:"end"` + Interval int64 `json:"interval"` // Interval is expected to be in milliseconds +} + +func newTimeRangeAndInterval(from, to time.Time, interval time.Duration) timeRangeAndInterval { + return timeRangeAndInterval{ + Start: mlTime(from), + End: mlTime(to), + Interval: interval.Milliseconds(), + } +} + +// mlTime is a time.Time that is marshalled as a string in a format is supported by Machine Learning API +type mlTime time.Time + +func (t *mlTime) UnmarshalJSON(b []byte) error { + s := strings.Trim(string(b), "\"") + parsed, err := time.Parse(timeFormat, s) + if err != nil { + return err + } + *t = mlTime(parsed) + return nil +} + +// MarshalJSON implements the Marshaler interface. +func (t mlTime) MarshalJSON() ([]byte, error) { + return []byte("\"" + time.Time(t).Format(timeFormat) + "\""), nil +} + +// outlierResponse is a model that represents a response of the outlier proxy API. +type outlierResponse struct { + Status string `json:"status"` + Data *backend.QueryDataResponse `json:"data,omitempty"` + Error string `json:"error,omitempty"` +} diff --git a/pkg/expr/ml/node.go b/pkg/expr/ml/node.go new file mode 100644 index 00000000000..096ce77d239 --- /dev/null +++ b/pkg/expr/ml/node.go @@ -0,0 +1,63 @@ +package ml + +import ( + "fmt" + "strings" + "time" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + jsoniter "github.com/json-iterator/go" + + "github.com/grafana/grafana/pkg/api/response" +) + +var json = jsoniter.ConfigCompatibleWithStandardLibrary + +type CommandType string + +const ( + Outlier CommandType = "outlier" + + // format of the time used by outlier API + timeFormat = "2006-01-02T15:04:05.999999999" + + defaultInterval = 1000 * time.Millisecond +) + +// Command is an interface implemented by all Machine Learning commands that can be executed against ML API. +type Command interface { + // DatasourceUID returns UID of a data source that is used by machine learning as the source of data + DatasourceUID() string + // Execute creates a payload send request to the ML API by calling the function argument sendRequest, and then parses response. + // Function sendRequest is supposed to abstract the client configuration such creating http request, adding authorization parameters, host etc. + Execute(from, to time.Time, sendRequest func(method string, path string, payload []byte) (response.Response, error)) (*backend.QueryDataResponse, error) +} + +// UnmarshalCommand parses a config parameters and creates a command. Requires key `type` to be specified. +// Based on the value of `type` field it parses a Command +func UnmarshalCommand(query []byte, appURL string) (Command, error) { + var expr CommandConfiguration + err := json.Unmarshal(query, &expr) + if err != nil { + return nil, fmt.Errorf("failed to unmarshall Machine learning command: %w", err) + } + if len(expr.Type) == 0 { + return nil, fmt.Errorf("required field 'type' is not specified or empty. Should be one of [%s]", Outlier) + } + + if len(expr.Config) == 0 { + return nil, fmt.Errorf("required field 'config' is not specified") + } + + var cmd Command + switch mlType := strings.ToLower(expr.Type); mlType { + case string(Outlier): + cmd, err = unmarshalOutlierCommand(expr, appURL) + default: + return nil, fmt.Errorf("unsupported command type. Should be one of [%s]", Outlier) + } + if err != nil { + return nil, fmt.Errorf("failed to unmarshal Machine learning %s command: %w", expr.Type, err) + } + return cmd, nil +} diff --git a/pkg/expr/ml/node_test.go b/pkg/expr/ml/node_test.go new file mode 100644 index 00000000000..e62faff0aa3 --- /dev/null +++ b/pkg/expr/ml/node_test.go @@ -0,0 +1,167 @@ +package ml + +import ( + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" +) + +func TestUnmarshalCommand(t *testing.T) { + appURL := "https://grafana.com" + + updateJson := func(cmd string, f func(m map[string]interface{})) func(t *testing.T) []byte { + return func(t *testing.T) []byte { + var d map[string]interface{} + require.NoError(t, json.UnmarshalFromString(cmd, &d)) + f(d) + data, err := json.Marshal(d) + require.NoError(t, err) + return data + } + } + t.Run("should parse outlier command", func(t *testing.T) { + cmd, err := UnmarshalCommand([]byte(outlierQuery), appURL) + require.NoError(t, err) + require.IsType(t, &OutlierCommand{}, cmd) + outlier := cmd.(*OutlierCommand) + require.Equal(t, 1234*time.Millisecond, outlier.interval) + require.Equal(t, appURL, outlier.appURL) + require.Equal(t, OutlierCommandConfiguration{ + DatasourceType: "prometheus", + DatasourceUID: "a4ce599c-4c93-44b9-be5b-76385b8c01be", + QueryParams: map[string]interface{}{ + "expr": "go_goroutines{}", + "range": true, + "refId": "A", + }, + Algorithm: map[string]interface{}{ + "name": "dbscan", + "config": map[string]interface{}{ + "epsilon": 7.667, + }, + "sensitivity": 0.83, + }, + ResponseType: "binary", + }, outlier.config) + }) + t.Run("should fallback to default if 'intervalMs' is not specified", func(t *testing.T) { + data := updateJson(outlierQuery, func(m map[string]interface{}) { + delete(m, "intervalMs") + })(t) + cmd, err := UnmarshalCommand(data, appURL) + require.NoError(t, err) + outlier := cmd.(*OutlierCommand) + require.Equal(t, defaultInterval, outlier.interval) + }) + t.Run("fails when", func(t *testing.T) { + testCases := []struct { + name string + config func(t *testing.T) []byte + err string + }{ + { + name: "field 'type' is missing", + config: updateJson(outlierQuery, func(cmd map[string]interface{}) { + delete(cmd, "type") + }), + err: "required field 'type' is not specified or empty. Should be one of [outlier]", + }, + { + name: "field 'type' is not known", + config: updateJson(outlierQuery, func(cmd map[string]interface{}) { + cmd["type"] = uuid.NewString() + }), + err: "unsupported command type. Should be one of [outlier]", + }, + { + name: "field 'type' is not string", + config: updateJson(outlierQuery, func(cmd map[string]interface{}) { + cmd["type"] = map[string]interface{}{ + "data": 1, + } + }), + err: "failed to unmarshall Machine learning command", + }, + { + name: "field 'config' is missing", + config: updateJson(outlierQuery, func(cmd map[string]interface{}) { + delete(cmd, "config") + }), + err: "required field 'config' is not specified", + }, + { + name: "field 'intervalMs' is not number", + config: updateJson(outlierQuery, func(cmd map[string]interface{}) { + cmd["intervalMs"] = "test" + }), + err: "failed to unmarshall Machine learning command", + }, + { + name: "field 'config.datasource_uid' is not specified", + config: updateJson(outlierQuery, func(cmd map[string]interface{}) { + cfg := cmd["config"].(map[string]interface{}) + delete(cfg, "datasource_uid") + }), + err: "required field `config.datasource_uid` is not specified", + }, + { + name: "field 'config.algorithm' is not specified", + config: updateJson(outlierQuery, func(cmd map[string]interface{}) { + cfg := cmd["config"].(map[string]interface{}) + delete(cfg, "algorithm") + }), + err: "required field `config.algorithm` is not specified", + }, + { + name: "field 'config.response_type' is not specified", + config: updateJson(outlierQuery, func(cmd map[string]interface{}) { + cfg := cmd["config"].(map[string]interface{}) + delete(cfg, "response_type") + }), + err: "required field `config.response_type` is not specified", + }, + { + name: "fields 'config.query' and 'config.query_params' are not specified", + config: updateJson(outlierQuery, func(cmd map[string]interface{}) { + cfg := cmd["config"].(map[string]interface{}) + delete(cfg, "query") + delete(cfg, "query_params") + }), + err: "neither of required fields `config.query_params` or `config.query` are specified", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + _, err := UnmarshalCommand(testCase.config(t), appURL) + require.ErrorContains(t, err, testCase.err) + }) + } + }) +} + +const outlierQuery = ` +{ + "type": "outlier", + "intervalMs": 1234, + "config": { + "datasource_uid": "a4ce599c-4c93-44b9-be5b-76385b8c01be", + "datasource_type": "prometheus", + "query_params": { + "expr": "go_goroutines{}", + "range": true, + "refId": "A" + }, + "response_type": "binary", + "algorithm": { + "name": "dbscan", + "config": { + "epsilon": 7.667 + }, + "sensitivity": 0.83 + } + } +} +` diff --git a/pkg/expr/ml/outlier.go b/pkg/expr/ml/outlier.go new file mode 100644 index 00000000000..40a53446c26 --- /dev/null +++ b/pkg/expr/ml/outlier.go @@ -0,0 +1,116 @@ +package ml + +import ( + "fmt" + "net/http" + "time" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + + "github.com/grafana/grafana/pkg/api/response" +) + +// OutlierCommand implements Command that sends a request to outlier proxy API and converts response to backend.QueryDataResponse +type OutlierCommand struct { + config OutlierCommandConfiguration + appURL string + interval time.Duration +} + +var _ Command = OutlierCommand{} + +func (c OutlierCommand) DatasourceUID() string { + return c.config.DatasourceUID +} + +// Execute copies the original configuration JSON and appends (overwrites) a field "start_end_attributes" and "grafana_url" to the root object. +// The value of "start_end_attributes" is JSON object that configures time range and interval. +// The value of "grafana_url" is app URL that should be used by ML to query the data source. +// After payload is generated it sends it to POST /proxy/api/v1/outlier endpoint and parses the response. +// The proxy API normally responds with a structured data. It recognizes status 200 and 204 as successful result. +// Other statuses are considered unsuccessful and result in error. Tries to extract error from the structured payload. +// Otherwise, mentions the full message in error +func (c OutlierCommand) Execute(from, to time.Time, sendRequest func(method string, path string, payload []byte) (response.Response, error)) (*backend.QueryDataResponse, error) { + payload := outlierRequestBody{ + Data: outlierData{ + Attributes: outlierAttributes{ + OutlierCommandConfiguration: c.config, + GrafanaURL: c.appURL, + StartEndAttributes: newTimeRangeAndInterval(from, to, c.interval), + }, + }, + } + requestBody, err := json.Marshal(payload) + if err != nil { + return nil, err + } + resp, err := sendRequest(http.MethodPost, "/proxy/api/v1/outlier", requestBody) + + if err != nil { + return nil, fmt.Errorf("failed to call ML API: %w", err) + } + if resp == nil { + return nil, fmt.Errorf("response is nil") + } + + // Outlier proxy API usually returns all responses with this body. + var respData outlierResponse + + respBody := resp.Body() + err = json.Unmarshal(respBody, &respData) + if err != nil { + return nil, fmt.Errorf("unexpected format of the response from ML API, status: %d, response: %s", resp.Status(), respBody) + } + + if respData.Status == "error" { + return nil, fmt.Errorf("ML API responded with error: %s", respData.Error) + } + + if resp.Status() == http.StatusNoContent { + return nil, nil + } + + if resp.Status() == http.StatusOK { + return respData.Data, nil + } + + return nil, fmt.Errorf("unexpected status %d returned by ML API, response: %s", resp.Status(), respBody) +} + +// unmarshalOutlierCommand parses the CommandConfiguration.Config, validates data and produces OutlierCommand. +func unmarshalOutlierCommand(expr CommandConfiguration, appURL string) (*OutlierCommand, error) { + var cfg OutlierCommandConfiguration + err := json.Unmarshal(expr.Config, &cfg) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal outlier command: %w", err) + } + if len(cfg.DatasourceUID) == 0 { + return nil, fmt.Errorf("required field `config.datasource_uid` is not specified") + } + + if len(cfg.Query) == 0 && len(cfg.QueryParams) == 0 { + return nil, fmt.Errorf("neither of required fields `config.query_params` or `config.query` are specified") + } + + if len(cfg.ResponseType) == 0 { + return nil, fmt.Errorf("required field `config.response_type` is not specified") + } + + if len(cfg.Algorithm) == 0 { + return nil, fmt.Errorf("required field `config.algorithm` is not specified") + } + + interval := defaultInterval + if expr.IntervalMs != nil { + i := time.Duration(*expr.IntervalMs) * time.Millisecond + if i > 0 { + interval = i + } + } + + return &OutlierCommand{ + config: cfg, + interval: interval, + appURL: appURL, + }, nil +} diff --git a/pkg/expr/ml/outlier_test.go b/pkg/expr/ml/outlier_test.go new file mode 100644 index 00000000000..f48003eeca8 --- /dev/null +++ b/pkg/expr/ml/outlier_test.go @@ -0,0 +1,194 @@ +package ml + +import ( + "errors" + "fmt" + "net/http" + "testing" + "time" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/exp/slices" + + "github.com/grafana/grafana/pkg/api/response" +) + +func TestOutlierExec(t *testing.T) { + outlier := OutlierCommand{ + config: OutlierCommandConfiguration{ + DatasourceType: "prometheus", + DatasourceUID: "a4ce599c-4c93-44b9-be5b-76385b8c01be", + QueryParams: map[string]interface{}{ + "expr": "go_goroutines{}", + "range": true, + "refId": "A", + }, + Algorithm: map[string]interface{}{ + "name": "dbscan", + "config": map[string]interface{}{ + "epsilon": 7.667, + }, + "sensitivity": 0.83, + }, + ResponseType: "binary", + }, + appURL: "https://grafana.com", + interval: 1000 * time.Second, + } + + t.Run("should generate expected parameters for request", func(t *testing.T) { + to := time.Now().UTC() + from := to.Add(-10 * time.Hour) + + called := false + _, err := outlier.Execute(from, to, func(method string, path string, payload []byte) (response.Response, error) { + require.Equal(t, "POST", method) + require.Equal(t, "/proxy/api/v1/outlier", path) + + assert.JSONEq(t, fmt.Sprintf(`{ + "data": { + "attributes": { + "datasource_type": "prometheus", + "datasource_uid": "a4ce599c-4c93-44b9-be5b-76385b8c01be", + "query_params": { + "expr": "go_goroutines{}", + "range": true, + "refId": "A" + }, + "algorithm": { + "config": { + "epsilon": 7.667 + }, + "name": "dbscan", + "sensitivity": 0.83 + }, + "response_type": "binary", + "grafana_url": "https://grafana.com", + "start_end_attributes": { + "start": "%s", + "end": "%s", + "interval": 1000000 + } + } + } + }`, from.Format(timeFormat), to.Format(timeFormat)), string(payload)) + + called = true + return nil, nil + }) + require.Truef(t, called, "request function was not called") + require.ErrorContains(t, err, "response is nil") + }) + + successResponse := `{"status":"success","data":{"results":{"A":{"status":200,"frames":[{"schema":{"name":"test","fields":[{"name":"Time","type":"time","typeInfo":{"frame":"time.Time"}},{"name":"Value","type":"number","typeInfo":{"frame":"float64"},"labels":{"instance":"test"}}]},"data":{"values":[[1686945300000],[0]]}}]}}}}` + + testCases := []struct { + name string + response response.Response + assert func(t *testing.T, r *backend.QueryDataResponse, err error) + }{ + { + name: "should return parsed frames when 200", + response: response.CreateNormalResponse(nil, []byte(successResponse), http.StatusOK), + assert: func(t *testing.T, r *backend.QueryDataResponse, err error) { + require.NoError(t, err) + require.NotNil(t, r) + require.Contains(t, r.Responses, "A") + require.NoError(t, r.Responses["A"].Error) + require.Equal(t, backend.StatusOK, r.Responses["A"].Status) + require.Len(t, r.Responses["A"].Frames, 1) + }, + }, + { + name: "should return nil if 204", + response: response.CreateNormalResponse(nil, []byte(`{"status": "success"}`), http.StatusNoContent), + assert: func(t *testing.T, r *backend.QueryDataResponse, err error) { + require.NoError(t, err) + require.Nil(t, r) + }, + }, + { + name: "should return error if any status and body has status error", + response: response.CreateNormalResponse(nil, []byte(`{"status": "error"}`), 0), + assert: func(t *testing.T, r *backend.QueryDataResponse, err error) { + require.ErrorContains(t, err, "ML API responded with error") + require.Nil(t, r) + }, + }, + { + name: "should return error with explanations if any status and body has status error", + response: response.CreateNormalResponse(nil, []byte(`{"status": "error", "error": "test-error"}`), 0), + assert: func(t *testing.T, r *backend.QueryDataResponse, err error) { + require.ErrorContains(t, err, "ML API responded with error") + require.ErrorContains(t, err, "test-error") + require.Nil(t, r) + }, + }, + { + name: "should return error response is empty", + response: response.CreateNormalResponse(nil, []byte(``), 0), + assert: func(t *testing.T, r *backend.QueryDataResponse, err error) { + require.ErrorContains(t, err, "unexpected format of the response from ML API") + require.Nil(t, r) + }, + }, + { + name: "should return error response is not a valid JSON", + response: response.CreateNormalResponse(nil, []byte(`{`), 0), + assert: func(t *testing.T, r *backend.QueryDataResponse, err error) { + require.ErrorContains(t, err, "unexpected format of the response from ML API") + require.Nil(t, r) + }, + }, + { + name: "should error if response is nil and no error", + response: nil, + assert: func(t *testing.T, r *backend.QueryDataResponse, err error) { + require.ErrorContains(t, err, "response is nil") + require.Nil(t, r) + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + to := time.Now() + from := to.Add(-10 * time.Hour) + + resp, err := outlier.Execute(from, to, func(method string, path string, payload []byte) (response.Response, error) { + return tc.response, nil + }) + tc.assert(t, resp, err) + }) + } + + t.Run("should return error if status is not known", func(t *testing.T) { + knownStatuses := []int{http.StatusOK, http.StatusNoContent} + + for status := 100; status < 600; status++ { + if http.StatusText(status) == "" || slices.Contains(knownStatuses, status) { + continue + } + to := time.Now() + from := to.Add(-10 * time.Hour) + + resp, err := outlier.Execute(from, to, func(method string, path string, payload []byte) (response.Response, error) { + return response.CreateNormalResponse(nil, []byte(successResponse), status), nil + }) + require.ErrorContains(t, err, fmt.Sprintf("unexpected status %d returned by ML API", status)) + require.Nil(t, resp) + } + }) + + t.Run("should propagate error from request function", func(t *testing.T) { + to := time.Now() + from := to.Add(-10 * time.Hour) + + resp, err := outlier.Execute(from, to, func(method string, path string, payload []byte) (response.Response, error) { + return nil, errors.New("test-error") + }) + require.ErrorContains(t, err, "test-error") + require.Nil(t, resp) + }) +} diff --git a/pkg/expr/ml/testing.go b/pkg/expr/ml/testing.go new file mode 100644 index 00000000000..b10eeffe1a8 --- /dev/null +++ b/pkg/expr/ml/testing.go @@ -0,0 +1,44 @@ +package ml + +import ( + "time" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + + "github.com/grafana/grafana/pkg/api/response" +) + +type FakeCommand struct { + Method string + Path string + Payload []byte + Response *backend.QueryDataResponse + Error error + Recordings []struct { + From time.Time + To time.Time + Response response.Response + Error error + } +} + +var _ Command = &FakeCommand{} + +func (f *FakeCommand) DatasourceUID() string { + return "fake-ml-datasource" +} + +func (f *FakeCommand) Execute(from, to time.Time, executor func(method string, path string, payload []byte) (response.Response, error)) (*backend.QueryDataResponse, error) { + r, err := executor(f.Method, f.Path, f.Payload) + f.Recordings = append(f.Recordings, struct { + From time.Time + To time.Time + Response response.Response + Error error + }{From: from, To: to, Response: r, Error: err}) + + if err != nil { + return nil, err + } + return f.Response, f.Error +} diff --git a/pkg/expr/ml_test.go b/pkg/expr/ml_test.go new file mode 100644 index 00000000000..b7f7771668c --- /dev/null +++ b/pkg/expr/ml_test.go @@ -0,0 +1,219 @@ +package expr + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "testing" + "time" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/expr/ml" + "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" + "github.com/grafana/grafana/pkg/services/user" +) + +func TestMLNodeExecute(t *testing.T) { + timeNow := time.Now() + expectedOrgID := int64(123) + timeRange := RelativeTimeRange{ + From: -10 * time.Hour, + To: 0, + } + request := &Request{ + Headers: map[string]string{ + "test": "test", + }, + Debug: false, + OrgId: expectedOrgID, + Queries: nil, + User: &user.SignedInUser{ + UserID: 1, + }, + } + + expectedResponse := &backend.CallResourceResponse{ + Status: 200, + Headers: nil, + Body: []byte("test-response"), + } + + pluginsClient := &recordingCallResourceHandler{ + response: expectedResponse, + } + + pluginCtx := &fakePluginContextProvider{ + result: map[string]*backend.AppInstanceSettings{ + mlPluginID: { + JSONData: json.RawMessage(`{ "initialized": true }`), + }, + }, + } + + s := &Service{ + cfg: nil, + dataService: nil, + pCtxProvider: pluginCtx, + features: nil, + pluginsClient: pluginsClient, + tracer: nil, + metrics: newMetrics(nil), + } + + cmdResponse := data.NewFrame("test", + data.NewField("Time", nil, []time.Time{time.Unix(1, 0)}), + data.NewField("Value", nil, []*float64{fp(1)}), + ) + + cmd := &ml.FakeCommand{ + Method: http.MethodPost, + Path: "/test/ml", + Payload: []byte(`{}`), + Response: &backend.QueryDataResponse{ + Responses: map[string]backend.DataResponse{ + "A": { + Frames: data.Frames{ + cmdResponse, + }, + Status: backend.StatusOK, + }, + }, + }, + Error: nil, + } + + node := &MLNode{ + baseNode: baseNode{}, + command: cmd, + TimeRange: timeRange, + request: request, + } + + result, err := node.Execute(context.Background(), timeNow, nil, s) + + require.NoError(t, err) + require.NotNil(t, result) + require.NotEmpty(t, result.Values) + + t.Run("should get plugin context", func(t *testing.T) { + require.NotEmpty(t, pluginCtx.recordings) + require.Equal(t, "Get", pluginCtx.recordings[0].method) + require.Equal(t, mlPluginID, pluginCtx.recordings[0].params[0]) + require.Equal(t, request.User, pluginCtx.recordings[0].params[1]) + }) + + t.Run("should call command execute with correct parameters", func(t *testing.T) { + require.NotEmpty(t, cmd.Recordings) + rec := cmd.Recordings[0] + require.Equal(t, timeRange.AbsoluteTime(timeNow).From, rec.From) + require.Equal(t, timeRange.AbsoluteTime(timeNow).To, rec.To) + require.NotNil(t, rec.Response) + require.Equal(t, expectedResponse.Status, rec.Response.Status()) + require.Equal(t, expectedResponse.Body, rec.Response.Body()) + }) + + t.Run("should call plugin API", func(t *testing.T) { + require.NotEmpty(t, pluginsClient.recordings) + req := pluginsClient.recordings[0] + require.Equal(t, cmd.Payload, req.Body) + require.Equal(t, cmd.Path, req.Path) + require.Equal(t, cmd.Method, req.Method) + + require.NotNil(t, req.PluginContext) + require.Equal(t, mlPluginID, req.PluginContext.PluginID) + + t.Run("should append request headers to API call", func(t *testing.T) { + for key, value := range request.Headers { + require.Contains(t, req.Headers, key) + require.Equal(t, value, req.Headers[key][0]) + } + }) + }) + + t.Run("should fail if plugin is not installed", func(t *testing.T) { + s := &Service{ + cfg: nil, + dataService: nil, + pCtxProvider: &fakePluginContextProvider{ + errorResult: plugincontext.ErrPluginNotFound, + }, + features: nil, + pluginsClient: nil, + tracer: nil, + metrics: nil, + } + + _, err := node.Execute(context.Background(), timeNow, nil, s) + require.ErrorIs(t, err, errMLPluginDoesNotExist) + }) + + t.Run("should fail if plugin settings cannot be retrieved", func(t *testing.T) { + expectedErr := errors.New("test-error") + s := &Service{ + cfg: nil, + dataService: nil, + pCtxProvider: &fakePluginContextProvider{ + errorResult: expectedErr, + }, + features: nil, + pluginsClient: nil, + tracer: nil, + metrics: nil, + } + + _, err := node.Execute(context.Background(), timeNow, nil, s) + require.ErrorIs(t, err, expectedErr) + }) + + t.Run("should fail if plugin is not initialized", func(t *testing.T) { + s := &Service{ + cfg: nil, + dataService: nil, + pCtxProvider: &fakePluginContextProvider{ + result: map[string]*backend.AppInstanceSettings{ + mlPluginID: { + JSONData: json.RawMessage(`{}`), + }, + }, + }, + features: nil, + pluginsClient: nil, + tracer: nil, + metrics: nil, + } + + _, err := node.Execute(context.Background(), timeNow, nil, s) + require.ErrorIs(t, err, errMLPluginDoesNotExist) + }) + + t.Run("should return QueryError if command failed", func(t *testing.T) { + s := &Service{ + cfg: nil, + dataService: nil, + pCtxProvider: pluginCtx, + features: nil, + pluginsClient: pluginsClient, + tracer: nil, + metrics: newMetrics(nil), + } + + cmd := &ml.FakeCommand{ + Error: errors.New("failed to execute command"), + } + + node := &MLNode{ + baseNode: baseNode{}, + command: cmd, + TimeRange: timeRange, + request: request, + } + + _, err := node.Execute(context.Background(), timeNow, nil, s) + require.IsType(t, err, QueryError{}) + require.ErrorIs(t, err, cmd.Error) + }) +} diff --git a/pkg/expr/nodes.go b/pkg/expr/nodes.go index e9c622c7298..0846eee6ea0 100644 --- a/pkg/expr/nodes.go +++ b/pkg/expr/nodes.go @@ -47,6 +47,7 @@ type baseNode struct { type rawNode struct { RefID string `json:"refId"` Query map[string]interface{} + QueryRaw []byte QueryType string TimeRange TimeRange DataSource *datasources.DataSource diff --git a/pkg/expr/service.go b/pkg/expr/service.go index 16d625fc9a3..678be80c093 100644 --- a/pkg/expr/service.go +++ b/pkg/expr/service.go @@ -15,6 +15,7 @@ import ( "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" + "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" ) @@ -47,6 +48,9 @@ func NodeTypeFromDatasourceUID(uid string) NodeType { if IsDataSource(uid) { return TypeCMDNode } + if uid == MLDatasourceUID { + return TypeMLNode + } return TypeDatasourceNode } @@ -54,7 +58,7 @@ func NodeTypeFromDatasourceUID(uid string) NodeType { type Service struct { cfg *setting.Cfg dataService backend.QueryDataHandler - pCtxProvider *plugincontext.Provider + pCtxProvider pluginContextProvider features featuremgmt.FeatureToggles pluginsClient backend.CallResourceHandler @@ -63,6 +67,11 @@ type Service struct { metrics *metrics } +type pluginContextProvider interface { + Get(ctx context.Context, pluginID string, user *user.SignedInUser, orgID int64) (backend.PluginContext, error) + GetWithDataSource(ctx context.Context, pluginID string, user *user.SignedInUser, ds *datasources.DataSource) (backend.PluginContext, error) +} + func ProvideService(cfg *setting.Cfg, pluginClient plugins.Client, pCtxProvider *plugincontext.Provider, features featuremgmt.FeatureToggles, registerer prometheus.Registerer, tracer tracing.Tracer) *Service { return &Service{ @@ -117,6 +126,15 @@ func DataSourceModelFromNodeType(kind NodeType) (*datasources.DataSource, error) JsonData: simplejson.New(), SecureJsonData: make(map[string][]byte), }, nil + case TypeMLNode: + return &datasources.DataSource{ + ID: mlDatasourceID, + UID: MLDatasourceUID, + Name: DatasourceUID, + Type: mlPluginID, + JsonData: simplejson.New(), + SecureJsonData: make(map[string][]byte), + }, nil case TypeDatasourceNode: return nil, errors.New("cannot create expression data source for data source kind") default: diff --git a/pkg/expr/testing.go b/pkg/expr/testing.go new file mode 100644 index 00000000000..38fbda4bbb1 --- /dev/null +++ b/pkg/expr/testing.go @@ -0,0 +1,84 @@ +package expr + +import ( + "context" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + + "github.com/grafana/grafana/pkg/services/datasources" + "github.com/grafana/grafana/pkg/services/user" +) + +type fakePluginContextProvider struct { + recordings []struct { + method string + params []interface{} + } + result map[string]*backend.AppInstanceSettings + errorResult error +} + +var _ pluginContextProvider = &fakePluginContextProvider{} + +func (f *fakePluginContextProvider) Get(_ context.Context, pluginID string, user *user.SignedInUser, orgID int64) (backend.PluginContext, error) { + f.recordings = append(f.recordings, struct { + method string + params []interface{} + }{method: "Get", params: []interface{}{pluginID, user, orgID}}) + if f.errorResult != nil { + return backend.PluginContext{}, f.errorResult + } + var u *backend.User + if user != nil { + u = &backend.User{ + Login: user.Login, + Name: user.Name, + Email: user.Email, + } + } + return backend.PluginContext{ + OrgID: orgID, + PluginID: pluginID, + User: u, + AppInstanceSettings: f.result[pluginID], + DataSourceInstanceSettings: nil, + }, nil +} + +func (f *fakePluginContextProvider) GetWithDataSource(ctx context.Context, pluginID string, user *user.SignedInUser, ds *datasources.DataSource) (backend.PluginContext, error) { + f.recordings = append(f.recordings, struct { + method string + params []interface{} + }{method: "GetWithDataSource", params: []interface{}{pluginID, user, ds}}) + + if f.errorResult != nil { + return backend.PluginContext{}, f.errorResult + } + + orgId := int64(1) + if user != nil { + orgId = user.OrgID + } + r, err := f.Get(ctx, pluginID, user, orgId) + if ds != nil { + r.DataSourceInstanceSettings = &backend.DataSourceInstanceSettings{ + ID: ds.ID, + UID: ds.UID, + Type: ds.Type, + Name: ds.Name, + } + } + return r, err +} + +type recordingCallResourceHandler struct { + recordings []*backend.CallResourceRequest + response *backend.CallResourceResponse +} + +var _ backend.CallResourceHandler = &recordingCallResourceHandler{} + +func (f *recordingCallResourceHandler) CallResource(_ context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { + f.recordings = append(f.recordings, req) + return sender.Send(f.response) +} diff --git a/pkg/kinds/dashboard/dashboard_spec_gen.go b/pkg/kinds/dashboard/dashboard_spec_gen.go index dfeac2c4409..437e9ec96b9 100644 --- a/pkg/kinds/dashboard/dashboard_spec_gen.go +++ b/pkg/kinds/dashboard/dashboard_spec_gen.go @@ -612,13 +612,13 @@ type RangeMap struct { // Range to match against and the result to apply when the value is within the range Options struct { // Min value of the range. It can be null which means -Infinity - From *float32 `json:"from"` + From *float64 `json:"from"` // Result used as replacement with text and color when the value matches Result ValueMappingResult `json:"result"` // Max value of the range. It can be null which means +Infinity - To *float32 `json:"to"` + To *float64 `json:"to"` } `json:"options"` Type RangeMapType `json:"type"` } diff --git a/pkg/login/social/generic_oauth.go b/pkg/login/social/generic_oauth.go index 6a4f9219da4..220787cacce 100644 --- a/pkg/login/social/generic_oauth.go +++ b/pkg/login/social/generic_oauth.go @@ -11,8 +11,6 @@ import ( "strconv" "golang.org/x/oauth2" - - "github.com/grafana/grafana/pkg/services/featuremgmt" ) type SocialGenericOAuth struct { @@ -489,13 +487,6 @@ func (s *SocialGenericOAuth) FetchOrganizations(ctx context.Context, client *htt return logins, true } -func (s *SocialGenericOAuth) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string { - if s.features.IsEnabled(featuremgmt.FlagAccessTokenExpirationCheck) { - opts = append(opts, oauth2.AccessTypeOffline) - } - return s.SocialBase.AuthCodeURL(state, opts...) -} - func (s *SocialGenericOAuth) SupportBundleContent(bf *bytes.Buffer) error { bf.WriteString("## GenericOAuth specific configuration\n\n") bf.WriteString("```ini\n") diff --git a/pkg/login/social/google_oauth.go b/pkg/login/social/google_oauth.go index 4e78f8a121e..763726925a1 100644 --- a/pkg/login/social/google_oauth.go +++ b/pkg/login/social/google_oauth.go @@ -114,7 +114,7 @@ func (s *SocialGoogle) extractFromAPI(ctx context.Context, client *http.Client) } func (s *SocialGoogle) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string { - if s.features.IsEnabled(featuremgmt.FlagAccessTokenExpirationCheck) { + if s.features.IsEnabled(featuremgmt.FlagAccessTokenExpirationCheck) && s.useRefreshToken { opts = append(opts, oauth2.AccessTypeOffline, oauth2.ApprovalForce) } return s.SocialBase.AuthCodeURL(state, opts...) diff --git a/pkg/login/social/social.go b/pkg/login/social/social.go index e307164b28c..2654e788fa1 100644 --- a/pkg/login/social/social.go +++ b/pkg/login/social/social.go @@ -17,6 +17,7 @@ import ( "strings" "time" + "golang.org/x/exp/slices" "golang.org/x/oauth2" "golang.org/x/text/cases" "golang.org/x/text/language" @@ -31,6 +32,10 @@ import ( "github.com/grafana/grafana/pkg/util" ) +const ( + OfflineAccessScope = "offline_access" +) + type SocialService struct { cfg *setting.Cfg @@ -66,6 +71,7 @@ type OAuthInfo struct { RoleAttributeStrict bool `toml:"role_attribute_strict"` TlsSkipVerify bool `toml:"tls_skip_verify"` UsePKCE bool `toml:"use_pkce"` + UseRefreshToken bool `toml:"use_refresh_token"` } func ProvideService(cfg *setting.Cfg, @@ -111,6 +117,7 @@ func ProvideService(cfg *setting.Cfg, TlsClientCa: sec.Key("tls_client_ca").String(), TlsSkipVerify: sec.Key("tls_skip_verify_insecure").MustBool(), UsePKCE: sec.Key("use_pkce").MustBool(), + UseRefreshToken: sec.Key("use_refresh_token").MustBool(false), AllowAssignGrafanaAdmin: sec.Key("allow_assign_grafana_admin").MustBool(false), AutoLogin: sec.Key("auto_login").MustBool(false), } @@ -198,6 +205,9 @@ func ProvideService(cfg *setting.Cfg, forceUseGraphAPI: sec.Key("force_use_graph_api").MustBool(false), skipOrgRoleSync: cfg.AzureADSkipOrgRoleSync, } + if info.UseRefreshToken && features.IsEnabled(featuremgmt.FlagAccessTokenExpirationCheck) { + appendUniqueScope(&config, OfflineAccessScope) + } } // Okta @@ -208,6 +218,9 @@ func ProvideService(cfg *setting.Cfg, allowedGroups: util.SplitString(sec.Key("allowed_groups").String()), skipOrgRoleSync: cfg.OktaSkipOrgRoleSync, } + if info.UseRefreshToken && features.IsEnabled(featuremgmt.FlagAccessTokenExpirationCheck) { + appendUniqueScope(&config, OfflineAccessScope) + } } // Generic - Uses the same scheme as GitHub. @@ -271,6 +284,7 @@ func (b *BasicUserInfo) String() string { b.Id, b.Name, b.Email, b.Login, b.Role, b.Groups) } +//go:generate mockery --name SocialConnector --structname MockSocialConnector --outpkg socialtest --filename social_connector_mock.go --output ../socialtest/ type SocialConnector interface { UserInfo(ctx context.Context, client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) IsEmailAllowed(email string) bool @@ -295,6 +309,7 @@ type SocialBase struct { autoAssignOrgRole string skipOrgRoleSync bool features featuremgmt.FeatureManager + useRefreshToken bool } type Error struct { @@ -344,6 +359,7 @@ func newSocialBase(name string, roleAttributeStrict: info.RoleAttributeStrict, skipOrgRoleSync: skipOrgRoleSync, features: features, + useRefreshToken: info.UseRefreshToken, } } @@ -592,3 +608,9 @@ func (s *SocialBase) retrieveRawIDToken(idToken interface{}) ([]byte, error) { return rawJSON, nil } + +func appendUniqueScope(config *oauth2.Config, scope string) { + if !slices.Contains(config.Scopes, OfflineAccessScope) { + config.Scopes = append(config.Scopes, OfflineAccessScope) + } +} diff --git a/pkg/login/socialtest/social_connector_mock.go b/pkg/login/socialtest/social_connector_mock.go new file mode 100644 index 00000000000..153b9df0966 --- /dev/null +++ b/pkg/login/socialtest/social_connector_mock.go @@ -0,0 +1,190 @@ +// Code generated by mockery v2.27.1. DO NOT EDIT. + +package socialtest + +import ( + bytes "bytes" + context "context" + + http "net/http" + + mock "github.com/stretchr/testify/mock" + + oauth2 "golang.org/x/oauth2" + + social "github.com/grafana/grafana/pkg/login/social" +) + +// MockSocialConnector is an autogenerated mock type for the SocialConnector type +type MockSocialConnector struct { + mock.Mock +} + +// AuthCodeURL provides a mock function with given fields: state, opts +func (_m *MockSocialConnector) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, state) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 string + if rf, ok := ret.Get(0).(func(string, ...oauth2.AuthCodeOption) string); ok { + r0 = rf(state, opts...) + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// Client provides a mock function with given fields: ctx, t +func (_m *MockSocialConnector) Client(ctx context.Context, t *oauth2.Token) *http.Client { + ret := _m.Called(ctx, t) + + var r0 *http.Client + if rf, ok := ret.Get(0).(func(context.Context, *oauth2.Token) *http.Client); ok { + r0 = rf(ctx, t) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*http.Client) + } + } + + return r0 +} + +// Exchange provides a mock function with given fields: ctx, code, authOptions +func (_m *MockSocialConnector) Exchange(ctx context.Context, code string, authOptions ...oauth2.AuthCodeOption) (*oauth2.Token, error) { + _va := make([]interface{}, len(authOptions)) + for _i := range authOptions { + _va[_i] = authOptions[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, code) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *oauth2.Token + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, ...oauth2.AuthCodeOption) (*oauth2.Token, error)); ok { + return rf(ctx, code, authOptions...) + } + if rf, ok := ret.Get(0).(func(context.Context, string, ...oauth2.AuthCodeOption) *oauth2.Token); ok { + r0 = rf(ctx, code, authOptions...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*oauth2.Token) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, ...oauth2.AuthCodeOption) error); ok { + r1 = rf(ctx, code, authOptions...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// IsEmailAllowed provides a mock function with given fields: email +func (_m *MockSocialConnector) IsEmailAllowed(email string) bool { + ret := _m.Called(email) + + var r0 bool + if rf, ok := ret.Get(0).(func(string) bool); ok { + r0 = rf(email) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// IsSignupAllowed provides a mock function with given fields: +func (_m *MockSocialConnector) IsSignupAllowed() bool { + ret := _m.Called() + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// SupportBundleContent provides a mock function with given fields: _a0 +func (_m *MockSocialConnector) SupportBundleContent(_a0 *bytes.Buffer) error { + ret := _m.Called(_a0) + + var r0 error + if rf, ok := ret.Get(0).(func(*bytes.Buffer) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// TokenSource provides a mock function with given fields: ctx, t +func (_m *MockSocialConnector) TokenSource(ctx context.Context, t *oauth2.Token) oauth2.TokenSource { + ret := _m.Called(ctx, t) + + var r0 oauth2.TokenSource + if rf, ok := ret.Get(0).(func(context.Context, *oauth2.Token) oauth2.TokenSource); ok { + r0 = rf(ctx, t) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(oauth2.TokenSource) + } + } + + return r0 +} + +// UserInfo provides a mock function with given fields: ctx, client, token +func (_m *MockSocialConnector) UserInfo(ctx context.Context, client *http.Client, token *oauth2.Token) (*social.BasicUserInfo, error) { + ret := _m.Called(ctx, client, token) + + var r0 *social.BasicUserInfo + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *http.Client, *oauth2.Token) (*social.BasicUserInfo, error)); ok { + return rf(ctx, client, token) + } + if rf, ok := ret.Get(0).(func(context.Context, *http.Client, *oauth2.Token) *social.BasicUserInfo); ok { + r0 = rf(ctx, client, token) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*social.BasicUserInfo) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *http.Client, *oauth2.Token) error); ok { + r1 = rf(ctx, client, token) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type mockConstructorTestingTNewMockSocialConnector interface { + mock.TestingT + Cleanup(func()) +} + +// NewMockSocialConnector creates a new instance of MockSocialConnector. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewMockSocialConnector(t mockConstructorTestingTNewMockSocialConnector) *MockSocialConnector { + mock := &MockSocialConnector{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/login/socialtest/social_service_fake.go b/pkg/login/socialtest/social_service_fake.go new file mode 100644 index 00000000000..e4f9a9270e1 --- /dev/null +++ b/pkg/login/socialtest/social_service_fake.go @@ -0,0 +1,33 @@ +package socialtest + +import ( + "net/http" + + "github.com/grafana/grafana/pkg/login/social" +) + +type FakeSocialService struct { + ExpectedAuthInfoProvider *social.OAuthInfo + ExpectedConnector social.SocialConnector + ExpectedHttpClient *http.Client +} + +func (fss *FakeSocialService) GetOAuthProviders() map[string]bool { + panic("not implemented") +} + +func (fss *FakeSocialService) GetOAuthHttpClient(string) (*http.Client, error) { + return fss.ExpectedHttpClient, nil +} + +func (fss *FakeSocialService) GetConnector(string) (social.SocialConnector, error) { + return fss.ExpectedConnector, nil +} + +func (fss *FakeSocialService) GetOAuthInfoProvider(string) *social.OAuthInfo { + return fss.ExpectedAuthInfoProvider +} + +func (fss *FakeSocialService) GetOAuthInfoProviders() map[string]*social.OAuthInfo { + panic("not implemented") +} diff --git a/pkg/plugins/manager/fakes/fakes.go b/pkg/plugins/manager/fakes/fakes.go index f099542360a..fd326d16f9c 100644 --- a/pkg/plugins/manager/fakes/fakes.go +++ b/pkg/plugins/manager/fakes/fakes.go @@ -233,16 +233,16 @@ func (r *FakePluginRepo) GetPluginArchiveInfo(ctx context.Context, pluginID, ver } type FakePluginStorage struct { - ExtractFunc func(_ context.Context, pluginID string, z *zip.ReadCloser) (*storage.ExtractedPluginArchive, error) + ExtractFunc func(_ context.Context, pluginID string, dirNameFunc storage.DirNameGeneratorFunc, z *zip.ReadCloser) (*storage.ExtractedPluginArchive, error) } func NewFakePluginStorage() *FakePluginStorage { return &FakePluginStorage{} } -func (s *FakePluginStorage) Extract(ctx context.Context, pluginID string, z *zip.ReadCloser) (*storage.ExtractedPluginArchive, error) { +func (s *FakePluginStorage) Extract(ctx context.Context, pluginID string, dirNameFunc storage.DirNameGeneratorFunc, z *zip.ReadCloser) (*storage.ExtractedPluginArchive, error) { if s.ExtractFunc != nil { - return s.ExtractFunc(ctx, pluginID, z) + return s.ExtractFunc(ctx, pluginID, dirNameFunc, z) } return &storage.ExtractedPluginArchive{}, nil } diff --git a/pkg/plugins/manager/installer.go b/pkg/plugins/manager/installer.go index 834beca8088..947a2f4326b 100644 --- a/pkg/plugins/manager/installer.go +++ b/pkg/plugins/manager/installer.go @@ -18,27 +18,29 @@ import ( var _ plugins.Installer = (*PluginInstaller)(nil) type PluginInstaller struct { - pluginRepo repo.Service - pluginStorage storage.ZipExtractor - pluginRegistry registry.Service - pluginLoader loader.Service - log log.Logger + pluginRepo repo.Service + pluginStorage storage.ZipExtractor + pluginStorageDirFunc storage.DirNameGeneratorFunc + pluginRegistry registry.Service + pluginLoader loader.Service + log log.Logger } func ProvideInstaller(cfg *config.Cfg, pluginRegistry registry.Service, pluginLoader loader.Service, pluginRepo repo.Service) *PluginInstaller { return New(pluginRegistry, pluginLoader, pluginRepo, - storage.FileSystem(log.NewPrettyLogger("installer.fs"), cfg.PluginsPath)) + storage.FileSystem(log.NewPrettyLogger("installer.fs"), cfg.PluginsPath), storage.SimpleDirNameGeneratorFunc) } func New(pluginRegistry registry.Service, pluginLoader loader.Service, pluginRepo repo.Service, - pluginStorage storage.ZipExtractor) *PluginInstaller { + pluginStorage storage.ZipExtractor, pluginStorageDirFunc storage.DirNameGeneratorFunc) *PluginInstaller { return &PluginInstaller{ - pluginLoader: pluginLoader, - pluginRegistry: pluginRegistry, - pluginRepo: pluginRepo, - pluginStorage: pluginStorage, - log: log.New("plugin.installer"), + pluginLoader: pluginLoader, + pluginRegistry: pluginRegistry, + pluginRepo: pluginRepo, + pluginStorage: pluginStorage, + pluginStorageDirFunc: pluginStorageDirFunc, + log: log.New("plugin.installer"), } } @@ -102,7 +104,7 @@ func (m *PluginInstaller) Add(ctx context.Context, pluginID, version string, opt } } - extractedArchive, err := m.pluginStorage.Extract(ctx, pluginID, pluginArchive.File) + extractedArchive, err := m.pluginStorage.Extract(ctx, pluginID, m.pluginStorageDirFunc, pluginArchive.File) if err != nil { return err } @@ -116,7 +118,7 @@ func (m *PluginInstaller) Add(ctx context.Context, pluginID, version string, opt return fmt.Errorf("%v: %w", fmt.Sprintf("failed to download plugin %s from repository", dep.ID), err) } - depArchive, err := m.pluginStorage.Extract(ctx, dep.ID, d.File) + depArchive, err := m.pluginStorage.Extract(ctx, dep.ID, m.pluginStorageDirFunc, d.File) if err != nil { return err } diff --git a/pkg/plugins/manager/installer_test.go b/pkg/plugins/manager/installer_test.go index a0a6043d186..a9af72e8985 100644 --- a/pkg/plugins/manager/installer_test.go +++ b/pkg/plugins/manager/installer_test.go @@ -53,7 +53,7 @@ func TestPluginManager_Add_Remove(t *testing.T) { } fs := &fakes.FakePluginStorage{ - ExtractFunc: func(_ context.Context, id string, z *zip.ReadCloser) (*storage.ExtractedPluginArchive, error) { + ExtractFunc: func(_ context.Context, id string, _ storage.DirNameGeneratorFunc, z *zip.ReadCloser) (*storage.ExtractedPluginArchive, error) { require.Equal(t, pluginID, id) require.Equal(t, mockZipV1, z) return &storage.ExtractedPluginArchive{ @@ -62,7 +62,7 @@ func TestPluginManager_Add_Remove(t *testing.T) { }, } - inst := New(fakes.NewFakePluginRegistry(), loader, pluginRepo, fs) + inst := New(fakes.NewFakePluginRegistry(), loader, pluginRepo, fs, storage.SimpleDirNameGeneratorFunc) err := inst.Add(context.Background(), pluginID, v1, testCompatOpts()) require.NoError(t, err) @@ -108,7 +108,7 @@ func TestPluginManager_Add_Remove(t *testing.T) { File: mockZipV2, }, nil } - fs.ExtractFunc = func(_ context.Context, pluginID string, z *zip.ReadCloser) (*storage.ExtractedPluginArchive, error) { + fs.ExtractFunc = func(_ context.Context, pluginID string, _ storage.DirNameGeneratorFunc, z *zip.ReadCloser) (*storage.ExtractedPluginArchive, error) { require.Equal(t, pluginV1.ID, pluginID) require.Equal(t, mockZipV2, z) return &storage.ExtractedPluginArchive{ @@ -168,7 +168,7 @@ func TestPluginManager_Add_Remove(t *testing.T) { }, } - pm := New(reg, &fakes.FakeLoader{}, &fakes.FakePluginRepo{}, &fakes.FakePluginStorage{}) + pm := New(reg, &fakes.FakeLoader{}, &fakes.FakePluginRepo{}, &fakes.FakePluginStorage{}, storage.SimpleDirNameGeneratorFunc) err := pm.Add(context.Background(), p.ID, "3.2.0", testCompatOpts()) require.ErrorIs(t, err, plugins.ErrInstallCorePlugin) diff --git a/pkg/plugins/manager/loader/finder/local.go b/pkg/plugins/manager/loader/finder/local.go index a2734c7adcb..e09b96e8750 100644 --- a/pkg/plugins/manager/loader/finder/local.go +++ b/pkg/plugins/manager/loader/finder/local.go @@ -28,15 +28,15 @@ type Local struct { production bool } -func NewLocalFinder(cfg *config.Cfg) *Local { +func NewLocalFinder(devMode bool) *Local { return &Local{ - production: !cfg.DevMode, + production: !devMode, log: log.New("local.finder"), } } func ProvideLocalFinder(cfg *config.Cfg) *Local { - return NewLocalFinder(cfg) + return NewLocalFinder(cfg.DevMode) } func (l *Local) Find(ctx context.Context, src plugins.PluginSource) ([]*plugins.FoundBundle, error) { @@ -79,10 +79,6 @@ func (l *Local) Find(ctx context.Context, src plugins.PluginSource) ([]*plugins. continue } - if _, dupe := foundPlugins[filepath.Dir(pluginJSONAbsPath)]; dupe { - l.log.Warn("Skipping plugin loading as it's a duplicate", "pluginID", plugin.ID) - continue - } foundPlugins[filepath.Dir(pluginJSONAbsPath)] = plugin } diff --git a/pkg/plugins/manager/loader/finder/local_test.go b/pkg/plugins/manager/loader/finder/local_test.go index e64dfb4434b..907920d24b2 100644 --- a/pkg/plugins/manager/loader/finder/local_test.go +++ b/pkg/plugins/manager/loader/finder/local_test.go @@ -255,7 +255,7 @@ func TestFinder_Find(t *testing.T) { } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - f := NewLocalFinder(pCfg) + f := NewLocalFinder(pCfg.DevMode) pluginBundles, err := f.Find(context.Background(), &fakes.FakePluginSource{ PluginURIsFunc: func(ctx context.Context) []string { return tc.pluginDirs @@ -292,7 +292,7 @@ func TestFinder_getAbsPluginJSONPaths(t *testing.T) { walk = origWalk }) - finder := NewLocalFinder(pCfg) + finder := NewLocalFinder(pCfg.DevMode) paths, err := finder.getAbsPluginJSONPaths("test") require.NoError(t, err) require.Empty(t, paths) @@ -307,7 +307,7 @@ func TestFinder_getAbsPluginJSONPaths(t *testing.T) { walk = origWalk }) - finder := NewLocalFinder(pCfg) + finder := NewLocalFinder(pCfg.DevMode) paths, err := finder.getAbsPluginJSONPaths("test") require.NoError(t, err) require.Empty(t, paths) @@ -322,7 +322,7 @@ func TestFinder_getAbsPluginJSONPaths(t *testing.T) { walk = origWalk }) - finder := NewLocalFinder(pCfg) + finder := NewLocalFinder(pCfg.DevMode) paths, err := finder.getAbsPluginJSONPaths("test") require.Error(t, err) require.Empty(t, paths) diff --git a/pkg/plugins/manager/loader/loader_test.go b/pkg/plugins/manager/loader/loader_test.go index 24e9b8b1cd4..b93eac7daf0 100644 --- a/pkg/plugins/manager/loader/loader_test.go +++ b/pkg/plugins/manager/loader/loader_test.go @@ -1494,7 +1494,7 @@ func newLoader(t *testing.T, cfg *config.Cfg, cbs ...func(loader *Loader)) *Load require.NoError(t, err) l := New(cfg, &fakes.FakeLicensingService{}, signature.NewUnsignedAuthorizer(cfg), fakes.NewFakePluginRegistry(), fakes.NewFakeBackendProcessProvider(), fakes.NewFakeProcessManager(), fakes.NewFakeRoleRegistry(), - assetpath.ProvideService(pluginscdn.ProvideService(cfg)), finder.NewLocalFinder(cfg), + assetpath.ProvideService(pluginscdn.ProvideService(cfg)), finder.NewLocalFinder(cfg.DevMode), signature.ProvideService(statickey.New()), angularInspector, &fakes.FakeOauthService{}) for _, cb := range cbs { diff --git a/pkg/plugins/manager/manager_integration_test.go b/pkg/plugins/manager/manager_integration_test.go index 08fc07a945f..6350640774b 100644 --- a/pkg/plugins/manager/manager_integration_test.go +++ b/pkg/plugins/manager/manager_integration_test.go @@ -121,7 +121,7 @@ func TestIntegrationPluginManager(t *testing.T) { angularInspector, err := angularinspector.NewStaticInspector() require.NoError(t, err) l := loader.ProvideService(pCfg, lic, signature.NewUnsignedAuthorizer(pCfg), - reg, provider.ProvideService(coreRegistry), finder.NewLocalFinder(pCfg), fakes.NewFakeRoleRegistry(), + reg, provider.ProvideService(coreRegistry), finder.NewLocalFinder(pCfg.DevMode), fakes.NewFakeRoleRegistry(), assetpath.ProvideService(pluginscdn.ProvideService(pCfg)), signature.ProvideService(statickey.New()), angularInspector, &fakes.FakeOauthService{}) srcs := sources.ProvideService(cfg, pCfg) diff --git a/pkg/plugins/storage/fs.go b/pkg/plugins/storage/fs.go index 445ce07c3f9..779330239cb 100644 --- a/pkg/plugins/storage/fs.go +++ b/pkg/plugins/storage/fs.go @@ -32,14 +32,18 @@ func FileSystem(logger log.PrettyLogger, pluginsDir string) *FS { } } -func (fs *FS) Extract(ctx context.Context, pluginID string, pluginArchive *zip.ReadCloser) ( +var SimpleDirNameGeneratorFunc = func(pluginID string) string { + return pluginID +} + +func (fs *FS) Extract(ctx context.Context, pluginID string, dirNameFunc DirNameGeneratorFunc, pluginArchive *zip.ReadCloser) ( *ExtractedPluginArchive, error) { - pluginDir, err := fs.extractFiles(ctx, pluginArchive, pluginID) + pluginDir, err := fs.extractFiles(ctx, pluginArchive, pluginID, dirNameFunc) if err != nil { return nil, fmt.Errorf("%v: %w", "failed to extract plugin archive", err) } - pluginJSON, err := readPluginJSON(pluginID, pluginDir) + pluginJSON, err := readPluginJSON(pluginDir) if err != nil { return nil, fmt.Errorf("%v: %w", "failed to convert to plugin DTO", err) } @@ -62,8 +66,9 @@ func (fs *FS) Extract(ctx context.Context, pluginID string, pluginArchive *zip.R }, nil } -func (fs *FS) extractFiles(_ context.Context, pluginArchive *zip.ReadCloser, pluginID string) (string, error) { - installDir := filepath.Join(fs.pluginsDir, pluginID) +func (fs *FS) extractFiles(_ context.Context, pluginArchive *zip.ReadCloser, pluginID string, dirNameFunc DirNameGeneratorFunc) (string, error) { + pluginDirName := dirNameFunc(pluginID) + installDir := filepath.Join(fs.pluginsDir, pluginDirName) if _, err := os.Stat(installDir); !os.IsNotExist(err) { fs.log.Debugf("Removing existing installation of plugin %s", installDir) err = os.RemoveAll(installDir) @@ -92,7 +97,7 @@ func (fs *FS) extractFiles(_ context.Context, pluginArchive *zip.ReadCloser, plu zf.Name, fs.pluginsDir) } - dstPath := filepath.Clean(filepath.Join(fs.pluginsDir, removeGitBuildFromName(zf.Name, pluginID))) // lgtm[go/zipslip] + dstPath := filepath.Clean(filepath.Join(fs.pluginsDir, removeGitBuildFromName(zf.Name, pluginDirName))) // lgtm[go/zipslip] if zf.FileInfo().IsDir() { // We can ignore gosec G304 here since it makes sense to give all users read access @@ -220,7 +225,7 @@ func removeGitBuildFromName(filename, pluginID string) string { return reGitBuild.ReplaceAllString(filename, pluginID+"/") } -func readPluginJSON(pluginID, pluginDir string) (plugins.JSONData, error) { +func readPluginJSON(pluginDir string) (plugins.JSONData, error) { pluginPath := filepath.Join(pluginDir, "plugin.json") // It's safe to ignore gosec warning G304 since the file path suffix is hardcoded @@ -232,7 +237,7 @@ func readPluginJSON(pluginID, pluginDir string) (plugins.JSONData, error) { // nolint:gosec data, err = os.ReadFile(pluginPath) if err != nil { - return plugins.JSONData{}, fmt.Errorf("could not find plugin.json or dist/plugin.json for %s in %s", pluginID, pluginDir) + return plugins.JSONData{}, fmt.Errorf("could not find plugin.json or dist/plugin.json for in %s", pluginDir) } } diff --git a/pkg/plugins/storage/fs_test.go b/pkg/plugins/storage/fs_test.go index 900069f5a5c..fa504248dd1 100644 --- a/pkg/plugins/storage/fs_test.go +++ b/pkg/plugins/storage/fs_test.go @@ -27,25 +27,24 @@ func TestAdd(t *testing.T) { pluginID := "test-app" fs := FileSystem(log.NewTestPrettyLogger(), testDir) - archive, err := fs.Extract(context.Background(), pluginID, zipFile(t, "./testdata/plugin-with-symlinks.zip")) + archive, err := fs.Extract(context.Background(), pluginID, SimpleDirNameGeneratorFunc, zipFile(t, "./testdata/plugin-with-symlinks.zip")) require.NotNil(t, archive) require.NoError(t, err) // verify extracted contents - files, err := os.ReadDir(filepath.Join(testDir, pluginID)) + files, err := os.ReadDir(archive.Path) require.NoError(t, err) - file2, err := files[2].Info() - require.NoError(t, err) - file4, err := files[4].Info() - require.NoError(t, err) - require.Len(t, files, 6) require.Equal(t, files[0].Name(), "MANIFEST.txt") require.Equal(t, files[1].Name(), "dashboards") require.Equal(t, files[2].Name(), "extra") + file2, err := files[2].Info() + require.NoError(t, err) require.Equal(t, os.ModeSymlink, file2.Mode()&os.ModeSymlink) require.Equal(t, files[3].Name(), "plugin.json") require.Equal(t, files[4].Name(), "symlink_to_txt") + file4, err := files[4].Info() + require.NoError(t, err) require.Equal(t, os.ModeSymlink, file4.Mode()&os.ModeSymlink) require.Equal(t, files[5].Name(), "text.txt") } @@ -59,27 +58,27 @@ func TestExtractFiles(t *testing.T) { skipWindows(t) pluginID := "grafana-simple-json-datasource" - path, err := i.extractFiles(context.Background(), zipFile(t, "testdata/grafana-simple-json-datasource-ec18fa4da8096a952608a7e4c7782b4260b41bcf.zip"), pluginID) + path, err := i.extractFiles(context.Background(), zipFile(t, "testdata/grafana-simple-json-datasource-ec18fa4da8096a952608a7e4c7782b4260b41bcf.zip"), pluginID, SimpleDirNameGeneratorFunc) require.Equal(t, filepath.Join(pluginsDir, pluginID), path) require.NoError(t, err) // File in zip has permissions 755 - fileInfo, err := os.Stat(filepath.Join(pluginsDir, "grafana-simple-json-datasource", "simple-plugin_darwin_amd64")) + fileInfo, err := os.Stat(filepath.Join(path, "simple-plugin_darwin_amd64")) require.NoError(t, err) require.Equal(t, "-rwxr-xr-x", fileInfo.Mode().String()) // File in zip has permission 755 - fileInfo, err = os.Stat(pluginsDir + "/grafana-simple-json-datasource/simple-plugin_linux_amd64") + fileInfo, err = os.Stat(filepath.Join(path, "simple-plugin_linux_amd64")) require.NoError(t, err) require.Equal(t, "-rwxr-xr-x", fileInfo.Mode().String()) // File in zip has permission 644 - fileInfo, err = os.Stat(pluginsDir + "/grafana-simple-json-datasource/simple-plugin_windows_amd64.exe") + fileInfo, err = os.Stat(filepath.Join(path, "simple-plugin_windows_amd64.exe")) require.NoError(t, err) require.Equal(t, "-rwxr-xr-x", fileInfo.Mode().String()) // File in zip has permission 755 - fileInfo, err = os.Stat(pluginsDir + "/grafana-simple-json-datasource/non-plugin-binary") + fileInfo, err = os.Stat(filepath.Join(path, "non-plugin-binary")) require.NoError(t, err) require.Equal(t, "-rwxr-xr-x", fileInfo.Mode().String()) }) @@ -88,43 +87,43 @@ func TestExtractFiles(t *testing.T) { skipWindows(t) pluginID := "plugin-with-symlink" - path, err := i.extractFiles(context.Background(), zipFile(t, "testdata/plugin-with-symlink.zip"), pluginID) + path, err := i.extractFiles(context.Background(), zipFile(t, "testdata/plugin-with-symlink.zip"), pluginID, SimpleDirNameGeneratorFunc) require.Equal(t, filepath.Join(pluginsDir, pluginID), path) require.NoError(t, err) - _, err = os.Stat(pluginsDir + "/plugin-with-symlink/symlink_to_txt") + _, err = os.Stat(filepath.Join(path, "symlink_to_txt")) require.NoError(t, err) - target, err := filepath.EvalSymlinks(pluginsDir + "/plugin-with-symlink/symlink_to_txt") + target, err := filepath.EvalSymlinks(filepath.Join(path, "symlink_to_txt")) require.NoError(t, err) - require.Equal(t, pluginsDir+"/plugin-with-symlink/text.txt", target) + require.Equal(t, filepath.Join(path, "text.txt"), target) }) t.Run("Should extract directory with relative symlink", func(t *testing.T) { skipWindows(t) pluginID := "plugin-with-symlink-dir" - path, err := i.extractFiles(context.Background(), zipFile(t, "testdata/plugin-with-symlink-dir.zip"), pluginID) + path, err := i.extractFiles(context.Background(), zipFile(t, "testdata/plugin-with-symlink-dir.zip"), pluginID, SimpleDirNameGeneratorFunc) require.Equal(t, filepath.Join(pluginsDir, pluginID), path) require.NoError(t, err) - _, err = os.Stat(pluginsDir + "/plugin-with-symlink-dir/symlink_to_dir") + _, err = os.Stat(filepath.Join(path, "symlink_to_dir")) require.NoError(t, err) - target, err := filepath.EvalSymlinks(pluginsDir + "/plugin-with-symlink-dir/symlink_to_dir") + target, err := filepath.EvalSymlinks(filepath.Join(path, "symlink_to_dir")) require.NoError(t, err) - require.Equal(t, pluginsDir+"/plugin-with-symlink-dir/dir", target) + require.Equal(t, filepath.Join(path, "dir"), target) }) t.Run("Should not extract file with absolute symlink", func(t *testing.T) { skipWindows(t) pluginID := "plugin-with-absolute-symlink" - path, err := i.extractFiles(context.Background(), zipFile(t, "testdata/plugin-with-absolute-symlink.zip"), pluginID) + path, err := i.extractFiles(context.Background(), zipFile(t, "testdata/plugin-with-absolute-symlink.zip"), pluginID, SimpleDirNameGeneratorFunc) require.Equal(t, filepath.Join(pluginsDir, pluginID), path) require.NoError(t, err) - _, err = os.Stat(pluginsDir + "/plugin-with-absolute-symlink/test.txt") + _, err = os.Stat(filepath.Join(path, "/test.txt")) require.True(t, os.IsNotExist(err)) }) @@ -132,16 +131,16 @@ func TestExtractFiles(t *testing.T) { skipWindows(t) pluginID := "plugin-with-absolute-symlink-dir" - path, err := i.extractFiles(context.Background(), zipFile(t, "testdata/plugin-with-absolute-symlink-dir.zip"), pluginID) + path, err := i.extractFiles(context.Background(), zipFile(t, "testdata/plugin-with-absolute-symlink-dir.zip"), pluginID, SimpleDirNameGeneratorFunc) require.Equal(t, filepath.Join(pluginsDir, pluginID), path) require.NoError(t, err) - _, err = os.Stat(pluginsDir + "/plugin-with-absolute-symlink-dir/target") + _, err = os.Stat(filepath.Join(path, "/plugin-with-absolute-symlink-dir/target")) require.True(t, os.IsNotExist(err)) }) t.Run("Should detect if archive members point outside of the destination directory", func(t *testing.T) { - path, err := i.extractFiles(context.Background(), zipFile(t, "testdata/plugin-with-parent-member.zip"), "plugin-with-parent-member") + path, err := i.extractFiles(context.Background(), zipFile(t, "testdata/plugin-with-parent-member.zip"), "plugin-with-parent-member", SimpleDirNameGeneratorFunc) require.Empty(t, path) require.EqualError(t, err, fmt.Sprintf( `archive member "../member.txt" tries to write outside of plugin directory: %q, this can be a security risk`, @@ -150,7 +149,7 @@ func TestExtractFiles(t *testing.T) { }) t.Run("Should detect if archive members are absolute", func(t *testing.T) { - path, err := i.extractFiles(context.Background(), zipFile(t, "testdata/plugin-with-absolute-member.zip"), "plugin-with-absolute-member") + path, err := i.extractFiles(context.Background(), zipFile(t, "testdata/plugin-with-absolute-member.zip"), "plugin-with-absolute-member", SimpleDirNameGeneratorFunc) require.Empty(t, path) require.EqualError(t, err, fmt.Sprintf( `archive member "/member.txt" tries to write outside of plugin directory: %q, this can be a security risk`, diff --git a/pkg/plugins/storage/ifaces.go b/pkg/plugins/storage/ifaces.go index 027827cf89d..eb2f2bde8c7 100644 --- a/pkg/plugins/storage/ifaces.go +++ b/pkg/plugins/storage/ifaces.go @@ -6,5 +6,7 @@ import ( ) type ZipExtractor interface { - Extract(ctx context.Context, pluginID string, rc *zip.ReadCloser) (*ExtractedPluginArchive, error) + Extract(ctx context.Context, pluginID string, destDir DirNameGeneratorFunc, rc *zip.ReadCloser) (*ExtractedPluginArchive, error) } + +type DirNameGeneratorFunc = func(pluginID string) string diff --git a/pkg/services/authn/authnimpl/service.go b/pkg/services/authn/authnimpl/service.go index 387a1a11991..2afc7349bc4 100644 --- a/pkg/services/authn/authnimpl/service.go +++ b/pkg/services/authn/authnimpl/service.go @@ -158,7 +158,7 @@ func ProvideService( s.RegisterPostAuthHook(userSyncService.SyncLastSeenHook, 40) if features.IsEnabled(featuremgmt.FlagAccessTokenExpirationCheck) { - s.RegisterPostAuthHook(sync.ProvideOAuthTokenSync(oauthTokenService, sessionService).SyncOauthTokenHook, 60) + s.RegisterPostAuthHook(sync.ProvideOAuthTokenSync(oauthTokenService, sessionService, socialService).SyncOauthTokenHook, 60) } s.RegisterPostAuthHook(userSyncService.FetchSyncedUserHook, 100) diff --git a/pkg/services/authn/authnimpl/sync/oauth_token_sync.go b/pkg/services/authn/authnimpl/sync/oauth_token_sync.go index f12c1d21719..7bb8dbf95ca 100644 --- a/pkg/services/authn/authnimpl/sync/oauth_token_sync.go +++ b/pkg/services/authn/authnimpl/sync/oauth_token_sync.go @@ -3,10 +3,12 @@ package sync import ( "context" "errors" + "strings" "time" "github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/login/social" "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/oauthtoken" @@ -15,15 +17,19 @@ import ( ) var ( - errExpiredAccessToken = errutil.NewBase(errutil.StatusUnauthorized, "oauth.expired-token") + errExpiredAccessToken = errutil.NewBase( + errutil.StatusUnauthorized, + "oauth.expired-token", + errutil.WithPublicMessage("OAuth access token expired")) ) -func ProvideOAuthTokenSync(service oauthtoken.OAuthTokenService, sessionService auth.UserTokenService) *OAuthTokenSync { +func ProvideOAuthTokenSync(service oauthtoken.OAuthTokenService, sessionService auth.UserTokenService, socialService social.Service) *OAuthTokenSync { return &OAuthTokenSync{ log.New("oauth_token.sync"), localcache.New(maxOAuthTokenCacheTTL, 15*time.Minute), service, sessionService, + socialService, } } @@ -32,6 +38,7 @@ type OAuthTokenSync struct { cache *localcache.CacheService service oauthtoken.OAuthTokenService sessionService auth.UserTokenService + socialService social.Service } func (s *OAuthTokenSync) SyncOauthTokenHook(ctx context.Context, identity *authn.Identity, _ *authn.Request) error { @@ -64,6 +71,19 @@ func (s *OAuthTokenSync) SyncOauthTokenHook(ctx context.Context, identity *authn return nil } + // get the token's auth provider (f.e. azuread) + provider := strings.TrimPrefix(token.AuthModule, "oauth_") + currentOAuthInfo := s.socialService.GetOAuthInfoProvider(provider) + if currentOAuthInfo == nil { + s.log.Warn("OAuth provider not found", "provider", provider) + return nil + } + + // if refresh token handling is disabled for this provider, we can skip the hook + if !currentOAuthInfo.UseRefreshToken { + return nil + } + expires := token.OAuthExpiry.Round(0).Add(-oauthtoken.ExpiryDelta) // token has not expired, so we don't have to refresh it if !expires.Before(time.Now()) { @@ -78,14 +98,14 @@ func (s *OAuthTokenSync) SyncOauthTokenHook(ctx context.Context, identity *authn } if err := s.service.InvalidateOAuthTokens(ctx, token); err != nil { - s.log.FromContext(ctx).Error("Failed invalidate OAuth tokens", "id", identity.ID, "error", err) + s.log.FromContext(ctx).Error("Failed to invalidate OAuth tokens", "id", identity.ID, "error", err) } if err := s.sessionService.RevokeToken(ctx, identity.SessionToken, false); err != nil { s.log.FromContext(ctx).Error("Failed to revoke session token", "id", identity.ID, "tokenId", identity.SessionToken.Id, "error", err) } - return errExpiredAccessToken.Errorf("oauth access token could not be refreshed: %w", auth.ErrInvalidSessionToken) + return errExpiredAccessToken.Errorf("oauth access token could not be refreshed: %w", err) } return nil diff --git a/pkg/services/authn/authnimpl/sync/oauth_token_sync_test.go b/pkg/services/authn/authnimpl/sync/oauth_token_sync_test.go index efe61d8f765..75606ab8689 100644 --- a/pkg/services/authn/authnimpl/sync/oauth_token_sync_test.go +++ b/pkg/services/authn/authnimpl/sync/oauth_token_sync_test.go @@ -10,6 +10,8 @@ import ( "github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/login/social" + "github.com/grafana/grafana/pkg/login/socialtest" "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/auth/authtest" "github.com/grafana/grafana/pkg/services/authn" @@ -20,8 +22,9 @@ import ( func TestOauthTokenSync_SyncOAuthTokenHook(t *testing.T) { type testCase struct { - desc string - identity *authn.Identity + desc string + identity *authn.Identity + oauthInfo *social.OAuthInfo expectedHasEntryToken *login.UserAuth expectHasEntryCalled bool @@ -84,6 +87,13 @@ func TestOauthTokenSync_SyncOAuthTokenHook(t *testing.T) { expectRevokeTokenCalled: true, expectedHasEntryToken: &login.UserAuth{OAuthExpiry: time.Now().Add(-10 * time.Minute)}, expectedErr: errExpiredAccessToken, + }, { + desc: "should skip sync when use_refresh_token is disabled", + identity: &authn.Identity{ID: "user:1", SessionToken: &auth.UserToken{}, AuthenticatedBy: login.GitLabAuthModule}, + expectHasEntryCalled: true, + expectTryRefreshTokenCalled: false, + expectedHasEntryToken: &login.UserAuth{OAuthExpiry: time.Now().Add(-10 * time.Minute)}, + oauthInfo: &social.OAuthInfo{UseRefreshToken: false}, }, } @@ -118,11 +128,22 @@ func TestOauthTokenSync_SyncOAuthTokenHook(t *testing.T) { }, } + if tt.oauthInfo == nil { + tt.oauthInfo = &social.OAuthInfo{ + UseRefreshToken: true, + } + } + + socialService := &socialtest.FakeSocialService{ + ExpectedAuthInfoProvider: tt.oauthInfo, + } + sync := &OAuthTokenSync{ log: log.NewNopLogger(), cache: localcache.New(0, 0), service: service, sessionService: sessionService, + socialService: socialService, } err := sync.SyncOauthTokenHook(context.Background(), tt.identity, nil) diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index fccfc55ba0b..88da30310f7 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -647,5 +647,12 @@ var ( FrontendOnly: true, Owner: grafanaObservabilityMetricsSquad, }, + { + Name: "mlExpressions", + Description: "Enable support for Machine Learning in server-side expressions", + Stage: FeatureStageExperimental, + FrontendOnly: false, + Owner: grafanaAlertingSquad, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index b9eb5966cce..abdef73a443 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -94,3 +94,4 @@ prometheusIncrementalQueryInstrumentation,experimental,@grafana/observability-me logsExploreTableVisualisation,experimental,@grafana/observability-logs,false,false,false,true awsDatasourcesTempCredentials,experimental,@grafana/aws-datasources,false,false,false,false transformationsRedesign,experimental,@grafana/observability-metrics,false,false,false,true +mlExpressions,experimental,@grafana/alerting-squad,false,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 53d37a1667c..2b0ca030888 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -386,4 +386,8 @@ const ( // FlagTransformationsRedesign // Enables the transformations redesign FlagTransformationsRedesign = "transformationsRedesign" + + // FlagMlExpressions + // Enable support for Machine Learning in server-side expressions + FlagMlExpressions = "mlExpressions" ) diff --git a/pkg/services/ngalert/eval/eval.go b/pkg/services/ngalert/eval/eval.go index 78339b97bb0..77a3d5e6ae2 100644 --- a/pkg/services/ngalert/eval/eval.go +++ b/pkg/services/ngalert/eval/eval.go @@ -270,10 +270,10 @@ func getExprRequest(ctx EvaluationContext, data []models.AlertQuery, dsCacheServ ds, ok := datasources[q.DatasourceUID] if !ok { switch nodeType := expr.NodeTypeFromDatasourceUID(q.DatasourceUID); nodeType { - case expr.TypeCMDNode: - ds, err = expr.DataSourceModelFromNodeType(nodeType) case expr.TypeDatasourceNode: ds, err = dsCacheService.GetDatasourceByUID(ctx.Ctx, q.DatasourceUID, ctx.User, false /*skipCache*/) + default: + ds, err = expr.DataSourceModelFromNodeType(nodeType) } if err != nil { return nil, fmt.Errorf("failed to build query '%s': %w", q.RefID, err) @@ -622,15 +622,24 @@ func (e *evaluatorImpl) Validate(ctx EvaluationContext, condition models.Conditi return err } for _, query := range req.Queries { - if query.DataSource == nil || expr.NodeTypeFromDatasourceUID(query.DataSource.UID) != expr.TypeDatasourceNode { + if query.DataSource == nil { continue } - p, found := e.pluginsStore.Plugin(ctx.Ctx, query.DataSource.Type) - if !found { // technically this should fail earlier during datasource resolution phase. - return fmt.Errorf("datasource refID %s could not be found: %w", query.RefID, plugins.ErrPluginUnavailable) - } - if !p.Backend { - return fmt.Errorf("datasource refID %s is not a backend datasource", query.RefID) + switch expr.NodeTypeFromDatasourceUID(query.DataSource.UID) { + case expr.TypeDatasourceNode: + p, found := e.pluginsStore.Plugin(ctx.Ctx, query.DataSource.Type) + if !found { // technically this should fail earlier during datasource resolution phase. + return fmt.Errorf("datasource refID %s could not be found: %w", query.RefID, plugins.ErrPluginUnavailable) + } + if !p.Backend { + return fmt.Errorf("datasource refID %s is not a backend datasource", query.RefID) + } + case expr.TypeMLNode: + _, found := e.pluginsStore.Plugin(ctx.Ctx, query.DataSource.Type) + if !found { + return fmt.Errorf("datasource refID %s could not be found: %w", query.RefID, plugins.ErrPluginUnavailable) + } + case expr.TypeCMDNode: } } _, err = e.create(condition, req) diff --git a/pkg/services/oauthtoken/oauth_token_test.go b/pkg/services/oauthtoken/oauth_token_test.go index b3ec0933e7a..44b922ffea9 100644 --- a/pkg/services/oauthtoken/oauth_token_test.go +++ b/pkg/services/oauthtoken/oauth_token_test.go @@ -1,10 +1,8 @@ package oauthtoken import ( - "bytes" "context" "errors" - "net/http" "reflect" "testing" "time" @@ -15,7 +13,7 @@ import ( "golang.org/x/sync/singleflight" "github.com/grafana/grafana/pkg/infra/usagestats" - "github.com/grafana/grafana/pkg/login/social" + "github.com/grafana/grafana/pkg/login/socialtest" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/login/authinfoservice" "github.com/grafana/grafana/pkg/services/user" @@ -221,12 +219,12 @@ func TestService_TryTokenRefresh_DifferentAuthModuleForUser(t *testing.T) { socialConnector.AssertNotCalled(t, "TokenSource") } -func setupOAuthTokenService(t *testing.T) (*Service, *FakeAuthInfoStore, *MockSocialConnector) { +func setupOAuthTokenService(t *testing.T) (*Service, *FakeAuthInfoStore, *socialtest.MockSocialConnector) { t.Helper() - socialConnector := &MockSocialConnector{} - socialService := &FakeSocialService{ - connector: socialConnector, + socialConnector := &socialtest.MockSocialConnector{} + socialService := &socialtest.FakeSocialService{ + ExpectedConnector: socialConnector, } authInfoStore := &FakeAuthInfoStore{} @@ -239,74 +237,6 @@ func setupOAuthTokenService(t *testing.T) (*Service, *FakeAuthInfoStore, *MockSo }, authInfoStore, socialConnector } -type FakeSocialService struct { - httpClient *http.Client - connector *MockSocialConnector -} - -func (fss *FakeSocialService) GetOAuthProviders() map[string]bool { - panic("not implemented") -} - -func (fss *FakeSocialService) GetOAuthHttpClient(string) (*http.Client, error) { - return fss.httpClient, nil -} - -func (fss *FakeSocialService) GetConnector(string) (social.SocialConnector, error) { - return fss.connector, nil -} - -func (fss *FakeSocialService) GetOAuthInfoProvider(string) *social.OAuthInfo { - panic("not implemented") -} - -func (fss *FakeSocialService) GetOAuthInfoProviders() map[string]*social.OAuthInfo { - panic("not implemented") -} - -type MockSocialConnector struct { - mock.Mock -} - -func (m *MockSocialConnector) Type() int { - args := m.Called() - return args.Int(0) -} - -func (m *MockSocialConnector) UserInfo(ctx context.Context, client *http.Client, token *oauth2.Token) (*social.BasicUserInfo, error) { - args := m.Called(client, token) - return args.Get(0).(*social.BasicUserInfo), args.Error(1) -} - -func (m *MockSocialConnector) IsEmailAllowed(email string) bool { - panic("not implemented") -} - -func (m *MockSocialConnector) IsSignupAllowed() bool { - panic("not implemented") -} - -func (m *MockSocialConnector) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string { - panic("not implemented") -} - -func (m *MockSocialConnector) Exchange(ctx context.Context, code string, authOptions ...oauth2.AuthCodeOption) (*oauth2.Token, error) { - panic("not implemented") -} - -func (m *MockSocialConnector) Client(ctx context.Context, t *oauth2.Token) *http.Client { - panic("not implemented") -} - -func (m *MockSocialConnector) TokenSource(ctx context.Context, t *oauth2.Token) oauth2.TokenSource { - args := m.Called(ctx, t) - return args.Get(0).(oauth2.TokenSource) -} - -func (m *MockSocialConnector) SupportBundleContent(bf *bytes.Buffer) error { - return nil -} - type FakeAuthInfoStore struct { login.Store ExpectedError error diff --git a/pkg/setting/setting_test.go b/pkg/setting/setting_test.go index b1bd27b32bf..370505ed666 100644 --- a/pkg/setting/setting_test.go +++ b/pkg/setting/setting_test.go @@ -809,3 +809,78 @@ func TestRedactedValue(t *testing.T) { }) } } + +func TestHandleAWSSettings(t *testing.T) { + t.Run("Should set default auth providers if not defined", func(t *testing.T) { + cfg := NewCfg() + awsSection, err := cfg.Raw.NewSection("aws") + require.NoError(t, err) + _, err = awsSection.NewKey("allowed_auth_providers", "") + require.NoError(t, err) + + cfg.handleAWSConfig() + assert.Equal(t, []string{"default", "keys", "credentials"}, cfg.AWSAllowedAuthProviders) + }) + t.Run("Should pass on auth providers defined in config", func(t *testing.T) { + cfg := NewCfg() + awsSection, err := cfg.Raw.NewSection("aws") + require.NoError(t, err) + _, err = awsSection.NewKey("allowed_auth_providers", "keys, credentials") + require.NoError(t, err) + + cfg.handleAWSConfig() + assert.Equal(t, []string{"keys", "credentials"}, cfg.AWSAllowedAuthProviders) + }) + t.Run("Should set assume role to true if not defined", func(t *testing.T) { + cfg := NewCfg() + awsSection, err := cfg.Raw.NewSection("aws") + require.NoError(t, err) + _, err = awsSection.NewKey("assume_role_enabled", "") + require.NoError(t, err) + + cfg.handleAWSConfig() + assert.Equal(t, true, cfg.AWSAssumeRoleEnabled) + }) + t.Run("Should set assume role to true if defined as true in the config", func(t *testing.T) { + cfg := NewCfg() + awsSection, err := cfg.Raw.NewSection("aws") + require.NoError(t, err) + _, err = awsSection.NewKey("assume_role_enabled", "true") + require.NoError(t, err) + + cfg.handleAWSConfig() + assert.Equal(t, true, cfg.AWSAssumeRoleEnabled) + }) + t.Run("Should set assume role to false if defined as false in the config", func(t *testing.T) { + cfg := NewCfg() + awsSection, err := cfg.Raw.NewSection("aws") + require.NoError(t, err) + _, err = awsSection.NewKey("assume_role_enabled", "false") + require.NoError(t, err) + + cfg.handleAWSConfig() + assert.Equal(t, false, cfg.AWSAssumeRoleEnabled) + }) + t.Run("Should set default page limit if not defined", func(t *testing.T) { + cfg := NewCfg() + awsSection, err := cfg.Raw.NewSection("aws") + require.NoError(t, err) + _, err = awsSection.NewKey("list_metrics_page_limit", "") + require.NoError(t, err) + + cfg.handleAWSConfig() + + assert.Equal(t, 500, cfg.AWSListMetricsPageLimit) + }) + t.Run("Should pass on the limit if it is defined in the config", func(t *testing.T) { + cfg := NewCfg() + awsSection, err := cfg.Raw.NewSection("aws") + require.NoError(t, err) + _, err = awsSection.NewKey("list_metrics_page_limit", "400") + require.NoError(t, err) + + cfg.handleAWSConfig() + + assert.Equal(t, 400, cfg.AWSListMetricsPageLimit) + }) +} diff --git a/pkg/tsdb/elasticsearch/response_parser.go b/pkg/tsdb/elasticsearch/response_parser.go index 065de02c40e..c8d1f946d61 100644 --- a/pkg/tsdb/elasticsearch/response_parser.go +++ b/pkg/tsdb/elasticsearch/response_parser.go @@ -102,7 +102,7 @@ func processLogsResponse(res *es.SearchResponse, target *Query, configuredFields for hitIdx, hit := range res.Hits.Hits { var flattened map[string]interface{} if hit["_source"] != nil { - flattened = flatten(hit["_source"].(map[string]interface{})) + flattened = flatten(hit["_source"].(map[string]interface{}), 10) } doc := map[string]interface{}{ @@ -174,7 +174,7 @@ func processRawDataResponse(res *es.SearchResponse, target *Query, configuredFie for hitIdx, hit := range res.Hits.Hits { var flattened map[string]interface{} if hit["_source"] != nil { - flattened = flatten(hit["_source"].(map[string]interface{})) + flattened = flatten(hit["_source"].(map[string]interface{}), 10) } doc := map[string]interface{}{ @@ -1041,38 +1041,26 @@ func getErrorFromElasticResponse(response *es.SearchResponse) string { } // flatten flattens multi-level objects to single level objects. It uses dot notation to join keys. -func flatten(target map[string]interface{}) map[string]interface{} { +func flatten(target map[string]interface{}, maxDepth int) map[string]interface{} { // On frontend maxDepth wasn't used but as we are processing on backend // let's put a limit to avoid infinite loop. 10 was chosen arbitrary. - maxDepth := 10 - currentDepth := 0 - delimiter := "" output := make(map[string]interface{}) + step(0, maxDepth, target, "", output) + return output +} - var step func(object map[string]interface{}, prev string) +func step(currentDepth, maxDepth int, target map[string]interface{}, prev string, output map[string]interface{}) { + nextDepth := currentDepth + 1 + for key, value := range target { + newKey := strings.Trim(prev+"."+key, ".") - step = func(object map[string]interface{}, prev string) { - for key, value := range object { - if prev == "" { - delimiter = "" - } else { - delimiter = "." - } - newKey := prev + delimiter + key - - v, ok := value.(map[string]interface{}) - shouldStepInside := ok && len(v) > 0 && currentDepth < maxDepth - if shouldStepInside { - currentDepth++ - step(v, newKey) - } else { - output[newKey] = value - } + v, ok := value.(map[string]interface{}) + if ok && len(v) > 0 && currentDepth < maxDepth { + step(nextDepth, maxDepth, v, newKey, output) + } else { + output[newKey] = value } } - - step(target, "") - return output } // sortPropNames orders propNames so that timeField is first (if it exists), log message field is second diff --git a/pkg/tsdb/elasticsearch/response_parser_test.go b/pkg/tsdb/elasticsearch/response_parser_test.go index 25b09ca1d76..a54e955eee3 100644 --- a/pkg/tsdb/elasticsearch/response_parser_test.go +++ b/pkg/tsdb/elasticsearch/response_parser_test.go @@ -2481,7 +2481,6 @@ func TestProcessBuckets(t *testing.T) { } ] } - `) result, err := queryDataTest(query, response) @@ -3305,7 +3304,7 @@ func TestFlatten(t *testing.T) { }, } - flattened := flatten(obj) + flattened := flatten(obj, 10) require.Len(t, flattened, 2) require.Equal(t, "bar", flattened["foo"]) require.Equal(t, "qux", flattened["nested.bax.baz"]) @@ -3340,10 +3339,111 @@ func TestFlatten(t *testing.T) { }, } - flattened := flatten(obj) + flattened := flatten(obj, 10) require.Len(t, flattened, 1) require.Equal(t, map[string]interface{}{"nested11": map[string]interface{}{"nested12": "abc"}}, flattened["nested0.nested1.nested2.nested3.nested4.nested5.nested6.nested7.nested8.nested9.nested10"]) }) + + t.Run("does not affect any non-nested JSON", func(t *testing.T) { + target := map[string]interface{}{ + "fieldName": "", + } + + assert.Equal(t, map[string]interface{}{ + "fieldName": "", + }, flatten(target, 10)) + }) + + t.Run("flattens up to maxDepth", func(t *testing.T) { + target := map[string]interface{}{ + "fieldName2": map[string]interface{}{ + "innerFieldName2": map[string]interface{}{ + "innerFieldName3": "", + }, + }, + } + + assert.Equal(t, map[string]interface{}{ + "fieldName2.innerFieldName2": map[string]interface{}{"innerFieldName3": ""}}, flatten(target, 1)) + }) + + t.Run("flattens up to maxDepth with multiple keys in target", func(t *testing.T) { + target := map[string]interface{}{ + "fieldName": map[string]interface{}{ + "innerFieldName": "", + }, + "fieldName2": map[string]interface{}{ + "innerFieldName2": map[string]interface{}{ + "innerFieldName3": "", + }, + }, + } + + assert.Equal(t, map[string]interface{}{"fieldName.innerFieldName": "", "fieldName2.innerFieldName2": map[string]interface{}{"innerFieldName3": ""}}, flatten(target, 1)) + }) + + t.Run("flattens multiple objects of the same max depth", func(t *testing.T) { + target := map[string]interface{}{ + "fieldName": map[string]interface{}{ + "innerFieldName": "", + }, + "fieldName2": map[string]interface{}{ + "innerFieldName2": "", + }, + } + + assert.Equal(t, map[string]interface{}{ + "fieldName.innerFieldName": "", + "fieldName2.innerFieldName2": ""}, flatten(target, 1)) + }) + + t.Run("only flattens multiple entries in the same key", func(t *testing.T) { + target := map[string]interface{}{ + "fieldName": map[string]interface{}{ + "innerFieldName": "", + "innerFieldName1": "", + }, + "fieldName2": map[string]interface{}{ + "innerFieldName2": map[string]interface{}{ + "innerFieldName3": "", + }, + }, + } + + assert.Equal(t, map[string]interface{}{ + "fieldName.innerFieldName": "", + "fieldName.innerFieldName1": "", + "fieldName2.innerFieldName2": map[string]interface{}{"innerFieldName3": ""}}, flatten(target, 1)) + }) + + t.Run("combines nested field names", func(t *testing.T) { + target := map[string]interface{}{ + "fieldName": map[string]interface{}{ + "innerFieldName": "", + }, + "fieldName2": map[string]interface{}{ + "innerFieldName2": "", + }, + } + + assert.Equal(t, map[string]interface{}{"fieldName.innerFieldName": "", "fieldName2.innerFieldName2": ""}, flatten(target, 10)) + }) + + t.Run("will preserve only one key with the same name", func(t *testing.T) { + // This test documents that in the unlikely case of a collision of a flattened name and an existing key, only + // one entry's value will be preserved at random + target := map[string]interface{}{ + "fieldName": map[string]interface{}{ + "innerFieldName": "one of these values will be lost", + }, + "fieldName.innerFieldName": "this may be lost", + } + + result := flatten(target, 10) + assert.Len(t, result, 1) + _, ok := result["fieldName.innerFieldName"] + assert.True(t, ok) + }) } func TestTrimEdges(t *testing.T) { diff --git a/public/app/core/components/NestedFolderPicker/NestedFolderList.tsx b/public/app/core/components/NestedFolderPicker/NestedFolderList.tsx index 8e83126abfc..9617668e356 100644 --- a/public/app/core/components/NestedFolderPicker/NestedFolderList.tsx +++ b/public/app/core/components/NestedFolderPicker/NestedFolderList.tsx @@ -6,6 +6,7 @@ import { GrafanaTheme2 } from '@grafana/data'; import { IconButton, useStyles2 } from '@grafana/ui'; import { getSvgSize } from '@grafana/ui/src/components/Icon/utils'; import { Text } from '@grafana/ui/src/components/Text/Text'; +import { Trans } from 'app/core/internationalization'; import { Indent } from 'app/features/browse-dashboards/components/Indent'; import { DashboardsTreeItem } from 'app/features/browse-dashboards/types'; import { DashboardViewItem } from 'app/features/search/types'; @@ -39,15 +40,21 @@ export function NestedFolderList({ return (
- - {Row} - + {items.length > 0 ? ( + + {Row} + + ) : ( +
+ No folders found +
+ )}
); } @@ -132,7 +139,6 @@ function Row({ index, style: virtualStyles, data }: RowProps) { )}