Merge remote-tracking branch 'origin/main' into ds-apiserver-with-configs

This commit is contained in:
Ryan McKinley
2025-08-26 08:41:07 +03:00
240 changed files with 6444 additions and 2345 deletions
-16
View File
@@ -2562,19 +2562,6 @@ exports[`better eslint`] = {
"public/app/features/live/centrifuge/channel.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/features/logs/components/panel/LogLineDetailsFields.tsx:5381": [
[0, 0, 0, "React Hook \\"useCallback\\" is called conditionally. React Hooks must be called in the exact same order in every component render.", "0"],
[0, 0, 0, "React Hook \\"useCallback\\" is called conditionally. React Hooks must be called in the exact same order in every component render.", "1"],
[0, 0, 0, "React Hook \\"useMemo\\" is called conditionally. React Hooks must be called in the exact same order in every component render.", "2"],
[0, 0, 0, "React Hook \\"useMemo\\" is called conditionally. React Hooks must be called in the exact same order in every component render.", "3"],
[0, 0, 0, "React Hook \\"useStyles2\\" is called conditionally. React Hooks must be called in the exact same order in every component render.", "4"],
[0, 0, 0, "React Hook \\"useStyles2\\" is called conditionally. React Hooks must be called in the exact same order in every component render.", "5"]
],
"public/app/features/logs/components/panel/LogList.tsx:5381": [
[0, 0, 0, "React Hook \\"useCallback\\" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return?", "0"],
[0, 0, 0, "React Hook \\"useCallback\\" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return?", "1"],
[0, 0, 0, "React Hook \\"useCallback\\" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return?", "2"]
],
"public/app/features/logs/logsFrame.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
@@ -2954,9 +2941,6 @@ exports[`better eslint`] = {
"public/app/features/transformers/editors/GroupToNestedTableTransformerEditor.tsx:5381": [
[0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"]
],
"public/app/features/transformers/editors/OrganizeFieldsTransformerEditor.tsx:5381": [
[0, 0, 0, "React Hook \\"useStyles2\\" is called conditionally. React Hooks must be called in the exact same order in every component render.", "0"]
],
"public/app/features/transformers/editors/ReduceTransformerEditor.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
+4 -4
View File
@@ -136,7 +136,7 @@
"name": "datasource/Tempo",
"action": "addToProject",
"addToProject": {
"url": "https://github.com/orgs/grafana/projects/221"
"url": "https://github.com/orgs/grafana/projects/457"
}
},
{
@@ -152,7 +152,7 @@
"name": "datasource/Parca",
"action": "addToProject",
"addToProject": {
"url": "https://github.com/orgs/grafana/projects/221"
"url": "https://github.com/orgs/grafana/projects/457"
}
},
{
@@ -168,7 +168,7 @@
"name": "datasource/Jaeger",
"action": "addToProject",
"addToProject": {
"url": "https://github.com/orgs/grafana/projects/221"
"url": "https://github.com/orgs/grafana/projects/457"
}
},
{
@@ -176,7 +176,7 @@
"name": "datasource/Zipkin",
"action": "addToProject",
"addToProject": {
"url": "https://github.com/orgs/grafana/projects/221"
"url": "https://github.com/orgs/grafana/projects/457"
}
},
{
+2
View File
@@ -16,6 +16,8 @@ permissions: {}
jobs:
detect-changes:
name: Detect whether code changed
# Run on `grafana/grafana` main branch, or on pull requests to prevent double-running on mirrors
if: (github.event_name == 'pull_request' || (github.event_name == 'push' && github.repository == 'grafana/grafana'))
runs-on: ubuntu-latest
permissions:
contents: read
+2
View File
@@ -14,6 +14,8 @@ permissions:
jobs:
detect-changes:
# Run on `grafana/grafana` main branch, or on pull requests to prevent double-running on mirrors
if: (github.event_name == 'pull_request' || (github.event_name == 'push' && github.repository == 'grafana/grafana'))
name: Detect whether code changed
runs-on: ubuntu-latest
permissions:
+2
View File
@@ -12,4 +12,6 @@ on:
jobs:
verify-i18n:
# Run on `grafana/grafana` main branch, or on pull requests to prevent double-running on mirrors
if: (github.event_name == 'pull_request' || (github.event_name == 'push' && github.repository == 'grafana/grafana'))
uses: grafana/grafana-github-actions/.github/workflows/verify-i18n.yml@main
+2
View File
@@ -20,6 +20,8 @@ permissions: {}
jobs:
docs:
# Run on `grafana/grafana` main branch, or on pull requests to prevent double-running on mirrors
if: (github.event_name == 'pull_request' || (github.event_name == 'push' && github.repository == 'grafana/grafana'))
name: Build & Verify Docs
runs-on: ubuntu-latest
+2
View File
@@ -20,6 +20,8 @@ env:
jobs:
detect-changes:
# Run on `grafana/grafana` main branch, or on pull requests to prevent double-running on mirrors
if: (github.event_name == 'pull_request' || (github.event_name == 'push' && github.repository == 'grafana/grafana'))
name: Detect whether code changed
runs-on: ubuntu-latest
permissions:
+3 -1
View File
@@ -10,6 +10,8 @@ permissions: {}
jobs:
detect-changes:
# Run on `grafana/grafana` main branch, or on pull requests to prevent double-running on mirrors
if: (github.event_name == 'pull_request' || (github.event_name == 'push' && github.repository == 'grafana/grafana'))
name: Detect whether code changed
runs-on: ubuntu-x64-small
permissions:
@@ -166,4 +168,4 @@ jobs:
with:
needs: ${{ toJson(needs) }}
failure-message: "One or more unit test jobs have failed"
success-message: "All unit tests completed successfully"
success-message: "All unit tests completed successfully"
@@ -19,6 +19,8 @@ permissions: {}
jobs:
detect-changes:
# Run on `grafana/grafana` main branch, or on pull requests to prevent double-running on mirrors
if: (github.event_name == 'pull_request' || (github.event_name == 'push' && github.repository == 'grafana/grafana'))
name: Detect whether code changed
runs-on: ubuntu-latest
permissions:
+3 -1
View File
@@ -12,6 +12,8 @@ permissions: {}
jobs:
reject-gh-secrets:
# Run on `grafana/grafana` main branch, or on pull requests to prevent double-running on mirrors
if: (github.event_name == 'pull_request' || (github.event_name == 'push' && github.repository == 'grafana/grafana'))
runs-on: ubuntu-latest
permissions:
contents: read
@@ -28,4 +30,4 @@ jobs:
echo "Found secrets access in the codebase. Please remove it in favour of Vault secrets."
echo "If you are sure this is correct, add '# nolint:reject-gh-secrets' to the end of the line. Be VERY careful with this."
exit 1
fi
fi
+4 -2
View File
@@ -49,7 +49,7 @@ jobs:
setup:
name: setup
runs-on: github-hosted-ubuntu-x64-small
if: github.repository == 'grafana/grafana'
if: (github.repository == 'grafana/grafana') || (github.repository == 'grafana/grafana-security-mirror' && contains(github.ref_name, '+security'))
outputs:
version: ${{ steps.output.outputs.version }}
grafana-commit: ${{ steps.output.outputs.grafana_commit }}
@@ -104,10 +104,11 @@ jobs:
BUCKET: grafana-prerelease
GRAFANA_COMMIT: ${{ needs.setup.outputs.grafana-commit }}
SOURCE_EVENT: ${{ inputs.source-event || github.event_name }}
REPO: ${{ github.repository }}
with:
github-token: ${{ steps.generate_token.outputs.token }}
script: |
const {REF, VERSION, BUILD_ID, BUCKET, GRAFANA_COMMIT, SOURCE_EVENT} = process.env;
const {REF, VERSION, BUILD_ID, BUCKET, GRAFANA_COMMIT, SOURCE_EVENT, REPO} = process.env;
await github.rest.actions.createWorkflowDispatch({
owner: 'grafana',
@@ -120,6 +121,7 @@ jobs:
"bucket": BUCKET,
"grafana-commit": GRAFANA_COMMIT,
"source-event": SOURCE_EVENT,
"upstream": REPO,
}
})
+2
View File
@@ -15,6 +15,8 @@ permissions: {}
jobs:
shellcheck:
# Run on `grafana/grafana` main branch, or on pull requests to prevent double-running on mirrors
if: (github.event_name == 'pull_request' || (github.event_name == 'push' && github.repository == 'grafana/grafana'))
name: Shellcheck scripts
runs-on: ubuntu-latest
permissions:
+2
View File
@@ -15,6 +15,8 @@ concurrency:
jobs:
detect-changes:
# Run on `grafana/grafana` main branch, or on pull requests to prevent double-running on mirrors
if: (github.event_name == 'pull_request' || (github.event_name == 'push' && github.repository == 'grafana/grafana'))
name: Detect whether code changed
runs-on: ubuntu-latest
permissions:
@@ -22,7 +22,8 @@ env:
jobs:
trigger-search-e2e:
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false
# Run on `grafana/grafana` main branch, or on pull requests to prevent double-running on mirrors
if: (github.event_name == 'pull_request' && github.event.pull_request.draft == false) || (github.event_name == 'push' && github.repository == 'grafana/grafana')
steps:
- name: Trigger Dashboard Search E2E
run: echo "Triggered Dashboard Search e2e..."
run: echo "Triggered Dashboard Search e2e..."
+1
View File
@@ -241,3 +241,4 @@ public/app/plugins/**/dist/
public/mockServiceWorker.js
/e2e-playwright/test-plugins/*/dist
/apps/provisioning/cmd/job-controller/bin/
+1 -1
View File
@@ -4,7 +4,7 @@ go 1.24.6
require (
github.com/grafana/grafana-app-sdk v0.40.3
github.com/grafana/grafana-app-sdk/logging v0.40.2
github.com/grafana/grafana-app-sdk/logging v0.40.3
k8s.io/apimachinery v0.33.3
k8s.io/apiserver v0.33.3
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff
+2 -2
View File
@@ -86,8 +86,8 @@ github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5T
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
github.com/grafana/grafana-app-sdk v0.40.3 h1:JFo7uAfbAJUfZ9neD7/4sODKm1xgu9zhckclH/N4DYU=
github.com/grafana/grafana-app-sdk v0.40.3/go.mod h1:j0KzHo3Sa6kd+lnwSScBNoV9Vobkg/YY9HtEjxpyPrk=
github.com/grafana/grafana-app-sdk/logging v0.40.2 h1:HQ1+y9Od92iMbWWB54QxiYpNtCvYGUVpyxvxZ7ywB1k=
github.com/grafana/grafana-app-sdk/logging v0.40.2/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU=
github.com/grafana/grafana-app-sdk/logging v0.40.3 h1:2VXsXXEQiqAavRP8wusRDB6rDqf5lufP7A6NfjELqPE=
github.com/grafana/grafana-app-sdk/logging v0.40.3/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU=
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI=
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 h1:uGoIog/wiQHI9GAxXO5TJbT0wWKH3O9HhOJW1F9c3fY=
+3 -3
View File
@@ -5,12 +5,14 @@ go 1.24.6
require (
cuelang.org/go v0.11.1
github.com/grafana/grafana-app-sdk v0.40.3
github.com/grafana/grafana-app-sdk/logging v0.40.3
github.com/grafana/grafana-plugin-sdk-go v0.278.0
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e
github.com/prometheus/client_golang v1.23.0
github.com/stretchr/testify v1.10.0
k8s.io/apimachinery v0.33.3
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff
k8s.io/utils v0.0.0-20241210054802-24370beab758
)
require (
@@ -22,7 +24,7 @@ require (
github.com/cheekybits/genny v1.0.0 // indirect
github.com/chromedp/cdproto v0.0.0-20250429231605-6ed5b53462d4 // indirect
github.com/cockroachdb/apd/v3 v3.2.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/elazarl/goproxy v1.7.2 // indirect
github.com/emicklei/go-restful/v3 v3.12.1 // indirect
@@ -45,7 +47,6 @@ require (
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/grafana/grafana-app-sdk/logging v0.40.2 // indirect
github.com/grafana/otel-profiling-go v0.5.1 // indirect
github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // indirect
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // indirect
@@ -129,7 +130,6 @@ require (
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/client-go v0.33.3 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect
+10 -4
View File
@@ -28,8 +28,8 @@ github.com/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEa
github.com/cockroachdb/apd/v3 v3.2.1/go.mod h1:klXJcjp+FffLTHlhIG69tezTDvdP065naDsHzKhYSqc=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
@@ -96,8 +96,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/grafana/grafana-app-sdk v0.40.3 h1:JFo7uAfbAJUfZ9neD7/4sODKm1xgu9zhckclH/N4DYU=
github.com/grafana/grafana-app-sdk v0.40.3/go.mod h1:j0KzHo3Sa6kd+lnwSScBNoV9Vobkg/YY9HtEjxpyPrk=
github.com/grafana/grafana-app-sdk/logging v0.40.2 h1:HQ1+y9Od92iMbWWB54QxiYpNtCvYGUVpyxvxZ7ywB1k=
github.com/grafana/grafana-app-sdk/logging v0.40.2/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU=
github.com/grafana/grafana-app-sdk/logging v0.40.3 h1:2VXsXXEQiqAavRP8wusRDB6rDqf5lufP7A6NfjELqPE=
github.com/grafana/grafana-app-sdk/logging v0.40.3/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU=
github.com/grafana/grafana-plugin-sdk-go v0.278.0 h1:5/rIYparLi02pofdaag8wnjspMMVNCi8cZhC4cdC3Ho=
github.com/grafana/grafana-plugin-sdk-go v0.278.0/go.mod h1:+8NXT/XUJ/89GV6FxGQ366NZ3nU+cAXDMd0OUESF9H4=
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e h1:BTKk7LHuG1kmAkucwTA7DuMbKpKvJTKrGdBmUNO4dfQ=
@@ -315,12 +315,14 @@ golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aC
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -340,10 +342,13 @@ golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -352,6 +357,7 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+5 -5
View File
@@ -20,13 +20,13 @@ ConversionStatus: {
// and the caller should instead fetch the stored version.
failed: bool
// The version which was stored when the dashboard was created / updated.
// Fetching this version should always succeed.
storedVersion: string
// The error message from the conversion.
// Empty if the conversion has not failed.
error: string
error?: string
// The version which was stored when the dashboard was created / updated.
// Fetching this version should always succeed.
storedVersion?: string
}
dashboard: {
@@ -9,12 +9,12 @@ type DashboardConversionStatus struct {
// If true, means that the dashboard is not valid,
// and the caller should instead fetch the stored version.
Failed bool `json:"failed"`
// The version which was stored when the dashboard was created / updated.
// Fetching this version should always succeed.
StoredVersion string `json:"storedVersion"`
// The error message from the conversion.
// Empty if the conversion has not failed.
Error string `json:"error"`
Error *string `json:"error,omitempty"`
// The version which was stored when the dashboard was created / updated.
// Fetching this version should always succeed.
StoredVersion *string `json:"storedVersion,omitempty"`
}
// NewDashboardConversionStatus creates a new DashboardConversionStatus object.
@@ -236,24 +236,22 @@ func schema_pkg_apis_dashboard_v0alpha1_DashboardConversionStatus(ref common.Ref
Format: "",
},
},
"storedVersion": {
"error": {
SchemaProps: spec.SchemaProps{
Description: "The version which was stored when the dashboard was created / updated. Fetching this version should always succeed.",
Default: "",
Description: "The error message from the conversion. Empty if the conversion has not failed.",
Type: []string{"string"},
Format: "",
},
},
"error": {
"storedVersion": {
SchemaProps: spec.SchemaProps{
Description: "The error message from the conversion. Empty if the conversion has not failed.",
Default: "",
Description: "The version which was stored when the dashboard was created / updated. Fetching this version should always succeed.",
Type: []string{"string"},
Format: "",
},
},
},
Required: []string{"failed", "storedVersion", "error"},
Required: []string{"failed"},
},
},
}
@@ -9,12 +9,12 @@ type DashboardConversionStatus struct {
// If true, means that the dashboard is not valid,
// and the caller should instead fetch the stored version.
Failed bool `json:"failed"`
// The version which was stored when the dashboard was created / updated.
// Fetching this version should always succeed.
StoredVersion string `json:"storedVersion"`
// The error message from the conversion.
// Empty if the conversion has not failed.
Error string `json:"error"`
Error *string `json:"error,omitempty"`
// The version which was stored when the dashboard was created / updated.
// Fetching this version should always succeed.
StoredVersion *string `json:"storedVersion,omitempty"`
}
// NewDashboardConversionStatus creates a new DashboardConversionStatus object.
@@ -229,24 +229,22 @@ func schema_pkg_apis_dashboard_v1beta1_DashboardConversionStatus(ref common.Refe
Format: "",
},
},
"storedVersion": {
"error": {
SchemaProps: spec.SchemaProps{
Description: "The version which was stored when the dashboard was created / updated. Fetching this version should always succeed.",
Default: "",
Description: "The error message from the conversion. Empty if the conversion has not failed.",
Type: []string{"string"},
Format: "",
},
},
"error": {
"storedVersion": {
SchemaProps: spec.SchemaProps{
Description: "The error message from the conversion. Empty if the conversion has not failed.",
Default: "",
Description: "The version which was stored when the dashboard was created / updated. Fetching this version should always succeed.",
Type: []string{"string"},
Format: "",
},
},
},
Required: []string{"failed", "storedVersion", "error"},
Required: []string{"failed"},
},
},
}
@@ -9,12 +9,12 @@ type DashboardConversionStatus struct {
// If true, means that the dashboard is not valid,
// and the caller should instead fetch the stored version.
Failed bool `json:"failed"`
// The version which was stored when the dashboard was created / updated.
// Fetching this version should always succeed.
StoredVersion string `json:"storedVersion"`
// The error message from the conversion.
// Empty if the conversion has not failed.
Error string `json:"error"`
Error *string `json:"error,omitempty"`
// The version which was stored when the dashboard was created / updated.
// Fetching this version should always succeed.
StoredVersion *string `json:"storedVersion,omitempty"`
}
// NewDashboardConversionStatus creates a new DashboardConversionStatus object.
@@ -1210,24 +1210,22 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardConversionStatus(ref common.Ref
Format: "",
},
},
"storedVersion": {
"error": {
SchemaProps: spec.SchemaProps{
Description: "The version which was stored when the dashboard was created / updated. Fetching this version should always succeed.",
Default: "",
Description: "The error message from the conversion. Empty if the conversion has not failed.",
Type: []string{"string"},
Format: "",
},
},
"error": {
"storedVersion": {
SchemaProps: spec.SchemaProps{
Description: "The error message from the conversion. Empty if the conversion has not failed.",
Default: "",
Description: "The version which was stored when the dashboard was created / updated. Fetching this version should always succeed.",
Type: []string{"string"},
Format: "",
},
},
},
Required: []string{"failed", "storedVersion", "error"},
Required: []string{"failed"},
},
},
}
@@ -9,12 +9,12 @@ type DashboardConversionStatus struct {
// If true, means that the dashboard is not valid,
// and the caller should instead fetch the stored version.
Failed bool `json:"failed"`
// The version which was stored when the dashboard was created / updated.
// Fetching this version should always succeed.
StoredVersion string `json:"storedVersion"`
// The error message from the conversion.
// Empty if the conversion has not failed.
Error string `json:"error"`
Error *string `json:"error,omitempty"`
// The version which was stored when the dashboard was created / updated.
// Fetching this version should always succeed.
StoredVersion *string `json:"storedVersion,omitempty"`
}
// NewDashboardConversionStatus creates a new DashboardConversionStatus object.
@@ -1215,24 +1215,22 @@ func schema_pkg_apis_dashboard_v2beta1_DashboardConversionStatus(ref common.Refe
Format: "",
},
},
"storedVersion": {
"error": {
SchemaProps: spec.SchemaProps{
Description: "The version which was stored when the dashboard was created / updated. Fetching this version should always succeed.",
Default: "",
Description: "The error message from the conversion. Empty if the conversion has not failed.",
Type: []string{"string"},
Format: "",
},
},
"error": {
"storedVersion": {
SchemaProps: spec.SchemaProps{
Description: "The error message from the conversion. Empty if the conversion has not failed.",
Default: "",
Description: "The version which was stored when the dashboard was created / updated. Fetching this version should always succeed.",
Type: []string{"string"},
Format: "",
},
},
},
Required: []string{"failed", "storedVersion", "error"},
Required: []string{"failed"},
},
},
}
@@ -4,14 +4,14 @@ import (
"k8s.io/apimachinery/pkg/conversion"
"k8s.io/apimachinery/pkg/runtime"
"github.com/grafana/grafana-app-sdk/logging"
dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1"
dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1"
dashv2beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1"
"github.com/grafana/grafana/pkg/infra/log"
)
var logger = log.New("dashboard.conversion")
var logger = logging.DefaultLogger.With("logger", "dashboard.conversion")
func RegisterConversions(s *runtime.Scheme) error {
// v0 conversions
@@ -9,8 +9,9 @@ import (
"testing"
"github.com/stretchr/testify/require"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"github.com/grafana/grafana/apps/dashboard/pkg/apis"
dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
@@ -27,7 +28,7 @@ func TestConversionMatrixExist(t *testing.T) {
// Initialize the migrator with a test data source provider
migration.Initialize(testutil.GetTestDataSourceProvider(), testutil.GetTestPanelProvider())
versions := []v1.Object{
versions := []metav1.Object{
&dashv0.Dashboard{Spec: common.Unstructured{Object: map[string]any{"title": "dashboardV0"}}},
&dashv1.Dashboard{Spec: common.Unstructured{Object: map[string]any{"title": "dashboardV1"}}},
&dashv2alpha1.Dashboard{Spec: dashv2alpha1.DashboardSpec{Title: "dashboardV2alpha1"}},
@@ -110,13 +111,13 @@ func TestDashboardConversionToAllVersions(t *testing.T) {
require.True(t, ok, "apiVersion not found or not a string")
// Parse group and version from apiVersion (format: "group/version")
parts := strings.Split(apiVersion, "/")
require.Equal(t, 2, len(parts), "apiVersion should be in format 'group/version'")
sourceVersion := parts[1]
gv, err := schema.ParseGroupVersion(apiVersion)
require.NoError(t, err)
require.Equal(t, dashv0.GROUP, gv.Group)
// Create source object based on version
var sourceDash v1.Object
switch sourceVersion {
var sourceDash metav1.Object
switch gv.Version {
case "v0alpha1":
var dash dashv0.Dashboard
err = json.Unmarshal(inputData, &dash)
@@ -134,7 +135,7 @@ func TestDashboardConversionToAllVersions(t *testing.T) {
err = json.Unmarshal(inputData, &dash)
sourceDash = &dash
default:
t.Fatalf("Unsupported source version: %s", sourceVersion)
t.Fatalf("Unsupported source version: %s", gv.Version)
}
require.NoError(t, err, "Failed to unmarshal dashboard into typed object")
@@ -157,22 +158,26 @@ func TestDashboardConversionToAllVersions(t *testing.T) {
if kind.Kind == "Dashboard" {
for _, version := range kind.Versions {
// Skip converting to the same version
if version.VersionName == sourceVersion {
if version.VersionName == gv.Version {
continue
}
filename := fmt.Sprintf("%s.%s.json", originalName, version.VersionName)
typeMeta := metav1.TypeMeta{
APIVersion: fmt.Sprintf("%s/%s", dashv0.APIGroup, version.VersionName),
Kind: kind.Kind, // Dashboard
}
// Create target object based on version
switch version.VersionName {
case "v0alpha1":
targetVersions[filename] = &dashv0.Dashboard{}
targetVersions[filename] = &dashv0.Dashboard{TypeMeta: typeMeta}
case "v1beta1":
targetVersions[filename] = &dashv1.Dashboard{}
targetVersions[filename] = &dashv1.Dashboard{TypeMeta: typeMeta}
case "v2alpha1":
targetVersions[filename] = &dashv2alpha1.Dashboard{}
targetVersions[filename] = &dashv2alpha1.Dashboard{TypeMeta: typeMeta}
case "v2beta1":
targetVersions[filename] = &dashv2beta1.Dashboard{}
targetVersions[filename] = &dashv2beta1.Dashboard{TypeMeta: typeMeta}
default:
t.Logf("Unknown version %s, skipping", version.VersionName)
}
@@ -192,14 +197,14 @@ func TestDashboardConversionToAllVersions(t *testing.T) {
require.NoError(t, err, "Conversion failed for %s", filename)
// Test the changes in the conversion result
testConversion(t, target.(v1.Object), filename, outDir)
testConversion(t, target.(metav1.Object), filename, outDir)
})
}
})
}
}
func testConversion(t *testing.T, convertedDash v1.Object, filename, outputDir string) {
func testConversion(t *testing.T, convertedDash metav1.Object, filename, outputDir string) {
t.Helper()
outPath := filepath.Join(outputDir, filename)
@@ -1,4 +1,6 @@
{
"kind": "Dashboard",
"apiVersion": "dashboard.grafana.app/v0alpha1",
"metadata": {
"name": "test-v2alpha1-complete",
"creationTimestamp": null,
@@ -13,8 +15,8 @@
"status": {
"conversion": {
"failed": true,
"storedVersion": "v2alpha1",
"error": "backend conversion not yet implemented"
"error": "backend conversion not yet implemented",
"storedVersion": "v2alpha1"
}
}
}
@@ -1,4 +1,6 @@
{
"kind": "Dashboard",
"apiVersion": "dashboard.grafana.app/v1beta1",
"metadata": {
"name": "test-v2alpha1-complete",
"creationTimestamp": null,
@@ -13,8 +15,8 @@
"status": {
"conversion": {
"failed": true,
"storedVersion": "v2alpha1",
"error": "backend conversion not yet implemented"
"error": "backend conversion not yet implemented",
"storedVersion": "v2alpha1"
}
}
}
@@ -504,8 +504,7 @@
"status": {
"conversion": {
"failed": false,
"storedVersion": "v2alpha1",
"error": ""
"storedVersion": "v2alpha1"
}
}
}
@@ -1,4 +1,6 @@
{
"kind": "Dashboard",
"apiVersion": "dashboard.grafana.app/v0alpha1",
"metadata": {
"name": "test-v2alpha1-annotations",
"creationTimestamp": null
@@ -7,8 +9,8 @@
"status": {
"conversion": {
"failed": true,
"storedVersion": "v2alpha1",
"error": "backend conversion not yet implemented"
"error": "backend conversion not yet implemented",
"storedVersion": "v2alpha1"
}
}
}
@@ -1,4 +1,6 @@
{
"kind": "Dashboard",
"apiVersion": "dashboard.grafana.app/v1beta1",
"metadata": {
"name": "test-v2alpha1-annotations",
"creationTimestamp": null
@@ -7,8 +9,8 @@
"status": {
"conversion": {
"failed": true,
"storedVersion": "v2alpha1",
"error": "backend conversion not yet implemented"
"error": "backend conversion not yet implemented",
"storedVersion": "v2alpha1"
}
}
}
@@ -1087,8 +1087,7 @@
"status": {
"conversion": {
"failed": false,
"storedVersion": "v2alpha1",
"error": ""
"storedVersion": "v2alpha1"
}
}
}
@@ -1,4 +1,6 @@
{
"kind": "Dashboard",
"apiVersion": "dashboard.grafana.app/v0alpha1",
"metadata": {
"name": "test-v2alpha1-groupby-adhoc-vars",
"creationTimestamp": null
@@ -7,8 +9,8 @@
"status": {
"conversion": {
"failed": true,
"storedVersion": "v2alpha1",
"error": "backend conversion not yet implemented"
"error": "backend conversion not yet implemented",
"storedVersion": "v2alpha1"
}
}
}
@@ -1,4 +1,6 @@
{
"kind": "Dashboard",
"apiVersion": "dashboard.grafana.app/v1beta1",
"metadata": {
"name": "test-v2alpha1-groupby-adhoc-vars",
"creationTimestamp": null
@@ -7,8 +9,8 @@
"status": {
"conversion": {
"failed": true,
"storedVersion": "v2alpha1",
"error": "backend conversion not yet implemented"
"error": "backend conversion not yet implemented",
"storedVersion": "v2alpha1"
}
}
}
@@ -93,8 +93,7 @@
"status": {
"conversion": {
"failed": false,
"storedVersion": "v2alpha1",
"error": ""
"storedVersion": "v2alpha1"
}
}
}
@@ -1,4 +1,6 @@
{
"kind": "Dashboard",
"apiVersion": "dashboard.grafana.app/v0alpha1",
"metadata": {
"name": "test-v2alpha1-viz-config",
"creationTimestamp": null
@@ -7,8 +9,8 @@
"status": {
"conversion": {
"failed": true,
"storedVersion": "v2alpha1",
"error": "backend conversion not yet implemented"
"error": "backend conversion not yet implemented",
"storedVersion": "v2alpha1"
}
}
}
@@ -1,4 +1,6 @@
{
"kind": "Dashboard",
"apiVersion": "dashboard.grafana.app/v1beta1",
"metadata": {
"name": "test-v2alpha1-viz-config",
"creationTimestamp": null
@@ -7,8 +9,8 @@
"status": {
"conversion": {
"failed": true,
"storedVersion": "v2alpha1",
"error": "backend conversion not yet implemented"
"error": "backend conversion not yet implemented",
"storedVersion": "v2alpha1"
}
}
}
@@ -215,8 +215,7 @@
"status": {
"conversion": {
"failed": false,
"storedVersion": "v2alpha1",
"error": ""
"storedVersion": "v2alpha1"
}
}
}
@@ -5,6 +5,7 @@ import (
"fmt"
"k8s.io/apimachinery/pkg/conversion"
"k8s.io/utils/ptr"
dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1"
@@ -21,13 +22,13 @@ func Convert_V0_to_V1(in *dashv0.Dashboard, out *dashv1.Dashboard, scope convers
out.Status = dashv1.DashboardStatus{
Conversion: &dashv1.DashboardConversionStatus{
StoredVersion: dashv0.VERSION,
StoredVersion: ptr.To(dashv0.VERSION),
},
}
if err := migration.Migrate(out.Spec.Object, schemaversion.LATEST_VERSION); err != nil {
out.Status.Conversion.Failed = true
out.Status.Conversion.Error = err.Error()
out.Status.Conversion.Error = ptr.To(err.Error())
// Classify error type for metrics
errorType := "conversion_error"
@@ -92,9 +93,9 @@ func Convert_V0_to_V2alpha1(in *dashv0.Dashboard, out *dashv2alpha1.Dashboard, s
out.Status = dashv2alpha1.DashboardStatus{
Conversion: &dashv2alpha1.DashboardConversionStatus{
StoredVersion: dashv0.VERSION,
StoredVersion: ptr.To(dashv0.VERSION),
Failed: true,
Error: "backend conversion not yet implemented",
Error: ptr.To("backend conversion not yet implemented"),
},
}
@@ -108,9 +109,9 @@ func Convert_V0_to_V2beta1(in *dashv0.Dashboard, out *dashv2beta1.Dashboard, sco
out.Status = dashv2beta1.DashboardStatus{
Conversion: &dashv2beta1.DashboardConversionStatus{
StoredVersion: dashv0.VERSION,
StoredVersion: ptr.To(dashv0.VERSION),
Failed: true,
Error: "backend conversion not yet implemented",
Error: ptr.To("backend conversion not yet implemented"),
},
}
@@ -2,6 +2,7 @@ package conversion
import (
"k8s.io/apimachinery/pkg/conversion"
"k8s.io/utils/ptr"
dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1"
@@ -16,7 +17,7 @@ func Convert_V1_to_V0(in *dashv1.Dashboard, out *dashv0.Dashboard, scope convers
out.Status = dashv0.DashboardStatus{
Conversion: &dashv0.DashboardConversionStatus{
StoredVersion: dashv1.VERSION,
StoredVersion: ptr.To(dashv1.VERSION),
},
}
@@ -45,9 +46,9 @@ func Convert_V1_to_V2alpha1(in *dashv1.Dashboard, out *dashv2alpha1.Dashboard, s
out.Status = dashv2alpha1.DashboardStatus{
Conversion: &dashv2alpha1.DashboardConversionStatus{
StoredVersion: dashv1.VERSION,
StoredVersion: ptr.To(dashv1.VERSION),
Failed: true,
Error: "backend conversion not yet implemented",
Error: ptr.To("backend conversion not yet implemented"),
},
}
@@ -61,9 +62,9 @@ func Convert_V1_to_V2beta1(in *dashv1.Dashboard, out *dashv2beta1.Dashboard, sco
out.Status = dashv2beta1.DashboardStatus{
Conversion: &dashv2beta1.DashboardConversionStatus{
StoredVersion: dashv1.VERSION,
StoredVersion: ptr.To(dashv1.VERSION),
Failed: true,
Error: "backend conversion not yet implemented",
Error: ptr.To("backend conversion not yet implemented"),
},
}
+14 -13
View File
@@ -2,6 +2,7 @@ package conversion
import (
"k8s.io/apimachinery/pkg/conversion"
"k8s.io/utils/ptr"
dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1"
@@ -16,9 +17,9 @@ func Convert_V2alpha1_to_V0(in *dashv2alpha1.Dashboard, out *dashv0.Dashboard, s
out.Status = dashv0.DashboardStatus{
Conversion: &dashv0.DashboardConversionStatus{
StoredVersion: dashv2alpha1.VERSION,
StoredVersion: ptr.To(dashv2alpha1.VERSION),
Failed: true,
Error: "backend conversion not yet implemented",
Error: ptr.To("backend conversion not yet implemented"),
},
}
@@ -32,9 +33,9 @@ func Convert_V2alpha1_to_V1(in *dashv2alpha1.Dashboard, out *dashv1.Dashboard, s
out.Status = dashv1.DashboardStatus{
Conversion: &dashv1.DashboardConversionStatus{
StoredVersion: dashv2alpha1.VERSION,
StoredVersion: ptr.To(dashv2alpha1.VERSION),
Failed: true,
Error: "backend conversion not yet implemented",
Error: ptr.To("backend conversion not yet implemented"),
},
}
@@ -48,9 +49,9 @@ func Convert_V2alpha1_to_V2beta1(in *dashv2alpha1.Dashboard, out *dashv2beta1.Da
if err := ConvertDashboard_V2alpha1_to_V2beta1(in, out, scope); err != nil {
out.Status = dashv2beta1.DashboardStatus{
Conversion: &dashv2beta1.DashboardConversionStatus{
StoredVersion: dashv2alpha1.VERSION,
StoredVersion: ptr.To(dashv2alpha1.VERSION),
Failed: true,
Error: err.Error(),
Error: ptr.To(err.Error()),
},
}
return err
@@ -59,7 +60,7 @@ func Convert_V2alpha1_to_V2beta1(in *dashv2alpha1.Dashboard, out *dashv2beta1.Da
// Set successful conversion status
out.Status = dashv2beta1.DashboardStatus{
Conversion: &dashv2beta1.DashboardConversionStatus{
StoredVersion: dashv2alpha1.VERSION,
StoredVersion: ptr.To(dashv2alpha1.VERSION),
Failed: false,
},
}
@@ -74,9 +75,9 @@ func Convert_V2beta1_to_V0(in *dashv2beta1.Dashboard, out *dashv0.Dashboard, sco
out.Status = dashv0.DashboardStatus{
Conversion: &dashv0.DashboardConversionStatus{
StoredVersion: dashv2beta1.VERSION,
StoredVersion: ptr.To(dashv2beta1.VERSION),
Failed: true,
Error: "backend conversion not yet implemented",
Error: ptr.To("backend conversion not yet implemented"),
},
}
@@ -90,9 +91,9 @@ func Convert_V2beta1_to_V1(in *dashv2beta1.Dashboard, out *dashv1.Dashboard, sco
out.Status = dashv1.DashboardStatus{
Conversion: &dashv1.DashboardConversionStatus{
StoredVersion: dashv2beta1.VERSION,
StoredVersion: ptr.To(dashv2beta1.VERSION),
Failed: true,
Error: "backend conversion not yet implemented",
Error: ptr.To("backend conversion not yet implemented"),
},
}
@@ -106,9 +107,9 @@ func Convert_V2beta1_to_V2alpha1(in *dashv2beta1.Dashboard, out *dashv2alpha1.Da
out.Status = dashv2alpha1.DashboardStatus{
Conversion: &dashv2alpha1.DashboardConversionStatus{
StoredVersion: dashv2beta1.VERSION,
StoredVersion: ptr.To(dashv2beta1.VERSION),
Failed: true,
Error: "backend conversion not yet implemented",
Error: ptr.To("backend conversion not yet implemented"),
},
}
+1 -1
View File
@@ -23,7 +23,7 @@ require (
github.com/go-test/deep v1.1.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/gnostic-models v0.6.9 // indirect
github.com/grafana/grafana-app-sdk/logging v0.40.2 // indirect
github.com/grafana/grafana-app-sdk/logging v0.40.3 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/josharian/intern v1.0.0 // indirect
+2 -2
View File
@@ -34,8 +34,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grafana/grafana-app-sdk v0.40.3 h1:JFo7uAfbAJUfZ9neD7/4sODKm1xgu9zhckclH/N4DYU=
github.com/grafana/grafana-app-sdk v0.40.3/go.mod h1:j0KzHo3Sa6kd+lnwSScBNoV9Vobkg/YY9HtEjxpyPrk=
github.com/grafana/grafana-app-sdk/logging v0.40.2 h1:HQ1+y9Od92iMbWWB54QxiYpNtCvYGUVpyxvxZ7ywB1k=
github.com/grafana/grafana-app-sdk/logging v0.40.2/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU=
github.com/grafana/grafana-app-sdk/logging v0.40.3 h1:2VXsXXEQiqAavRP8wusRDB6rDqf5lufP7A6NfjELqPE=
github.com/grafana/grafana-app-sdk/logging v0.40.3/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU=
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e h1:BTKk7LHuG1kmAkucwTA7DuMbKpKvJTKrGdBmUNO4dfQ=
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e/go.mod h1:IA4SOwun8QyST9c5UNs/fN37XL6boXXDvRYFcFwbipg=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+2 -2
View File
@@ -24,7 +24,7 @@ require (
github.com/grafana/authlib v0.0.0-20250710201142-9542f2f28d43
github.com/grafana/grafana v0.0.0-00010101000000-000000000000
github.com/grafana/grafana-app-sdk v0.40.3
github.com/grafana/grafana-app-sdk/logging v0.40.2
github.com/grafana/grafana-app-sdk/logging v0.40.3
github.com/grafana/grafana-app-sdk/plugin v0.40.3
github.com/grafana/grafana/apps/folder v0.0.0
github.com/grafana/grafana/pkg/apimachinery v0.0.0
@@ -81,7 +81,7 @@ require (
github.com/cloudflare/circl v1.6.1 // indirect
github.com/cockroachdb/apd/v3 v3.2.1 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/diegoholiveira/jsonlogic/v3 v3.7.4 // indirect
github.com/dlmiddlecote/sqlstats v1.0.2 // indirect
+4 -4
View File
@@ -336,8 +336,8 @@ github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cznic/b v0.0.0-20180115125044-35e9bbe41f07/go.mod h1:URriBxXwVq5ijiJ12C7iIZqlA69nTlI+LgI6/pwftG8=
github.com/cznic/fileutil v0.0.0-20180108211300-6a051e75936f/go.mod h1:8S58EK26zhXSxzv7NQFpnliaOQsmDUxvoQO3rt154Vg=
@@ -687,8 +687,8 @@ github.com/grafana/dskit v0.0.0-20250611075409-46f51e1ce914 h1:qcSGhr691f1mmPHwg
github.com/grafana/dskit v0.0.0-20250611075409-46f51e1ce914/go.mod h1:OiN4P4aC6LwLzLbEupH3Ue83VfQoNMfG48rsna8jI/E=
github.com/grafana/grafana-app-sdk v0.40.3 h1:JFo7uAfbAJUfZ9neD7/4sODKm1xgu9zhckclH/N4DYU=
github.com/grafana/grafana-app-sdk v0.40.3/go.mod h1:j0KzHo3Sa6kd+lnwSScBNoV9Vobkg/YY9HtEjxpyPrk=
github.com/grafana/grafana-app-sdk/logging v0.40.2 h1:HQ1+y9Od92iMbWWB54QxiYpNtCvYGUVpyxvxZ7ywB1k=
github.com/grafana/grafana-app-sdk/logging v0.40.2/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU=
github.com/grafana/grafana-app-sdk/logging v0.40.3 h1:2VXsXXEQiqAavRP8wusRDB6rDqf5lufP7A6NfjELqPE=
github.com/grafana/grafana-app-sdk/logging v0.40.3/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU=
github.com/grafana/grafana-app-sdk/plugin v0.40.3 h1:uH0oFZnYOUL+OXcyhd5NVYwoM+Wa0WUXvZ2Om1M91r0=
github.com/grafana/grafana-app-sdk/plugin v0.40.3/go.mod h1:+ylwE0P8WgPu5zURK5aDnVJpwRpuK3573rwrVV28qzQ=
github.com/grafana/grafana-aws-sdk v1.1.0 h1:G0fvwbQmHw14c5RXPd7Gnw9ZQcgzl139LtMDoe0KhmE=
+1 -1
View File
@@ -30,7 +30,7 @@ require (
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grafana/grafana-app-sdk/logging v0.40.2 // indirect
github.com/grafana/grafana-app-sdk/logging v0.40.3 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
+2 -2
View File
@@ -49,8 +49,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grafana/grafana-app-sdk v0.40.3 h1:JFo7uAfbAJUfZ9neD7/4sODKm1xgu9zhckclH/N4DYU=
github.com/grafana/grafana-app-sdk v0.40.3/go.mod h1:j0KzHo3Sa6kd+lnwSScBNoV9Vobkg/YY9HtEjxpyPrk=
github.com/grafana/grafana-app-sdk/logging v0.40.2 h1:HQ1+y9Od92iMbWWB54QxiYpNtCvYGUVpyxvxZ7ywB1k=
github.com/grafana/grafana-app-sdk/logging v0.40.2/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU=
github.com/grafana/grafana-app-sdk/logging v0.40.3 h1:2VXsXXEQiqAavRP8wusRDB6rDqf5lufP7A6NfjELqPE=
github.com/grafana/grafana-app-sdk/logging v0.40.3/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+1 -1
View File
@@ -30,7 +30,7 @@ require (
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grafana/grafana-app-sdk/logging v0.40.2 // indirect
github.com/grafana/grafana-app-sdk/logging v0.40.3 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
+2 -2
View File
@@ -49,8 +49,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grafana/grafana-app-sdk v0.40.3 h1:JFo7uAfbAJUfZ9neD7/4sODKm1xgu9zhckclH/N4DYU=
github.com/grafana/grafana-app-sdk v0.40.3/go.mod h1:j0KzHo3Sa6kd+lnwSScBNoV9Vobkg/YY9HtEjxpyPrk=
github.com/grafana/grafana-app-sdk/logging v0.40.2 h1:HQ1+y9Od92iMbWWB54QxiYpNtCvYGUVpyxvxZ7ywB1k=
github.com/grafana/grafana-app-sdk/logging v0.40.2/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU=
github.com/grafana/grafana-app-sdk/logging v0.40.3 h1:2VXsXXEQiqAavRP8wusRDB6rDqf5lufP7A6NfjELqPE=
github.com/grafana/grafana-app-sdk/logging v0.40.3/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+1 -1
View File
@@ -36,7 +36,7 @@ require (
github.com/google/uuid v1.6.0 // indirect
github.com/grafana/authlib v0.0.0-20250710201142-9542f2f28d43 // indirect
github.com/grafana/dskit v0.0.0-20250611075409-46f51e1ce914 // indirect
github.com/grafana/grafana-app-sdk/logging v0.40.2 // indirect
github.com/grafana/grafana-app-sdk/logging v0.40.3 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
+2 -2
View File
@@ -59,8 +59,8 @@ github.com/grafana/dskit v0.0.0-20250611075409-46f51e1ce914 h1:qcSGhr691f1mmPHwg
github.com/grafana/dskit v0.0.0-20250611075409-46f51e1ce914/go.mod h1:OiN4P4aC6LwLzLbEupH3Ue83VfQoNMfG48rsna8jI/E=
github.com/grafana/grafana-app-sdk v0.40.3 h1:JFo7uAfbAJUfZ9neD7/4sODKm1xgu9zhckclH/N4DYU=
github.com/grafana/grafana-app-sdk v0.40.3/go.mod h1:j0KzHo3Sa6kd+lnwSScBNoV9Vobkg/YY9HtEjxpyPrk=
github.com/grafana/grafana-app-sdk/logging v0.40.2 h1:HQ1+y9Od92iMbWWB54QxiYpNtCvYGUVpyxvxZ7ywB1k=
github.com/grafana/grafana-app-sdk/logging v0.40.2/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU=
github.com/grafana/grafana-app-sdk/logging v0.40.3 h1:2VXsXXEQiqAavRP8wusRDB6rDqf5lufP7A6NfjELqPE=
github.com/grafana/grafana-app-sdk/logging v0.40.3/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU=
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250428110029-a8ea72012bde h1:ydSrBIOCxJQ84+JU+cyYsOLL40QeXrB7rYfsY/ezU4w=
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250428110029-a8ea72012bde/go.mod h1:3MwgP0ISxGviTy3ZUJZsNz/56NNtHztMlH+gcxDt6Tw=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
@@ -0,0 +1,28 @@
.PHONY: build clean test
BINARY_NAME=job-controller
BUILD_DIR=bin
LDFLAGS=-w -s
build:
@echo "Building $(BINARY_NAME)..."
@mkdir -p $(BUILD_DIR)
go build -ldflags="$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME) .
clean:
@echo "Cleaning..."
@rm -rf $(BUILD_DIR)
run:
@echo "Running $(BINARY_NAME)..."
./$(BUILD_DIR)/$(BINARY_NAME)
install:
@echo "Installing $(BINARY_NAME)..."
go install .
help:
@echo "Available targets:"
@echo " build - Build the binary"
@echo " clean - Clean build artifacts"
@echo " run - Run the binary"
@echo " install - Install the binary"
@echo " help - Show this help"
@@ -0,0 +1,138 @@
# Jobs Controller
> [!WARNING]
> This controller has current limitations:
>
> - This binary does not start the ConcurrentJobDriver yet. Notifications are logged but not consumed by workers here.
> - Job processing (claim/renew/update/complete) isn't implemented yet as it requires refactoring of some components.
### Behavior
- Watches provisioning `Jobs` and emits notifications on job creation.
- Optionally cleans up `HistoricJobs` after a configurable expiration. Disable when job history is stored in Loki.
- Queueing and claiming:
- Creating a `Job` enqueues work. Drivers “claim” one job at a time under a time-bound lease so only one worker processes it at once.
- If a driver crashes or loses its lease, cleanup makes the job eligible to be claimed again. This yields at-least-once processing.
- New job notifications reduce latency; periodic ticks ensure progress even without notifications.
- Processing and status:
- A supporting worker processes the job, renewing the lease in the background. If lease renewal fails or expires, processing aborts.
- Status updates are persisted with conflict-aware retries. Progress is throttled to avoid excessive writes while still providing timely feedback.
- When processing finishes, the job is marked complete and a copy is written to history.
- Historic jobs role:
- Historic jobs are a read-only audit trail and UX surface for recent job outcomes, progress summaries, errors, and reference URLs.
- Retention is implementation-dependent: this controller can prune old history objects periodically, or history can be stored in Loki; when using Loki, disable local cleanup with `--history-expiration=0`.
This binary currently wires informers and emits job-create notifications. In the full setup, concurrent drivers consume notifications and execute workers to process jobs using the behavior above.
### Flags
- `--token` (string): Token to use for authentication against the provisioning API.
- `--token-exchange-url` (string): Token exchange endpoint used to mint the access token for the provisioning API.
- `--provisioning-server-url` (string): Base URL to the provisioning API server (e.g., `https://localhost:6446`).
- `--history-expiration` (duration): If greater than zero, enables HistoricJobs cleanup and sets the retention window (e.g., `30s`, `15m`, `24h`). If `0`, cleanup is disabled.
#### TLS Configuration
- `--tls-insecure` (bool): Skip TLS certificate verification. Default: `true` (for development/testing).
- `--tls-cert-file` (string): Path to TLS client certificate file for mutual TLS authentication.
- `--tls-key-file` (string): Path to TLS client private key file for mutual TLS authentication.
- `--tls-ca-file` (string): Path to TLS CA certificate file for server certificate verification.
### How to run
1. Build from this folder:
- `make build`
2. Ensure the following services are running locally: provisioning API server, secrets service API server, repository controller, unified storage, and auth.
3. Start the controller:
- Using Loki for job history:
- Ensure the Provisioning API is configured with Loki for job history (see `createJobHistoryConfigFromSettings` in `pkg/registry/apis/provisioning/register.go`).
- Run without history cleanup:
- `./bin/job-controller --token-exchange-url=http://localhost:6481/sign/access-token --token=ProvisioningAdminToken --provisioning-server-url=https://localhost:6446`
- Without Loki (local/dev or when Loki is unavailable):
- Run without cleanup:
- `./bin/job-controller --token-exchange-url=http://localhost:6481/sign/access-token --token=ProvisioningAdminToken --provisioning-server-url=https://localhost:6446`
- Or enable local HistoricJobs cleanup with a retention window:
- `./bin/job-controller --token-exchange-url=http://localhost:6481/sign/access-token --token=ProvisioningAdminToken --provisioning-server-url=https://localhost:6446 --history-expiration=30s`
#### TLS Configuration Examples
- **Production with proper TLS verification**:
```bash
./bin/job-controller \
--token-exchange-url=http://localhost:6481/sign/access-token \
--token=ProvisioningAdminToken \
--provisioning-server-url=https://provisioning.example.com:6446 \
--tls-insecure=false \
--tls-ca-file=/path/to/ca-cert.pem
```
- **Mutual TLS authentication**:
```bash
./bin/job-controller \
--token-exchange-url=http://localhost:6481/sign/access-token \
--token=ProvisioningAdminToken \
--provisioning-server-url=https://provisioning.example.com:6446 \
--tls-insecure=false \
--tls-ca-file=/path/to/ca-cert.pem \
--tls-cert-file=/path/to/client-cert.pem \
--tls-key-file=/path/to/client-key.pem
```
- **Development with self-signed certificates (insecure)**:
```bash
./bin/job-controller \
--token-exchange-url=http://localhost:6481/sign/access-token \
--token=ProvisioningAdminToken \
--provisioning-server-url=https://localhost:6446 \
--tls-insecure=true
```
### Expected behavior
1. Create a repository and enqueue a job (note that the repository must be marked as healthy):
```curl
export ACCESS_TOKEN=$(curl -X POST http://localhost:6481/sign/access-token \
-H "X-Realms: [{\"type\":\"system\",\"identifier\":\"system\"}]" \
-H "X-Org-ID: 0" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ProvisioningAdminToken" \
-d '{
"namespace": "*",
"audiences": ["provisioning.grafana.app"]
}' | jq -r '.data.token')
```
```curl
curl -X POST https://localhost:6446/apis/provisioning.grafana.app/v0alpha1/namespaces/default/repositories/test6/jobs \
-H "Content-Type: application/json" --insecure \
-H "X-Access-Token: Bearer $ACCESS_TOKEN" \
-d '{
"action": "pull",
"pull": {
"incremental": false
}
}'
```
2. The controller emits a notification on job creation.
```
➜ job-controller git:(feature/standalone-job-controller) ✗ ./bin/job-controller --token-exchange-url=http://localhost:6481/sign/access-token --token=ProvisioningAdminToken --provisioning-server-url=https://localhost:6446
{"time":"2025-08-21T14:27:03.789337+02:00","level":"INFO","msg":"job create notification received","logger":"provisioning-job-controller"}
```
```
```
3. In a full setup with the concurrent driver, workers claim and process jobs, updating status and writing history.
4. Entries move to `HistoricJobs`; if cleanup is enabled, older entries are pruned based on `--history-expiration`.
@@ -0,0 +1,234 @@
package main
import (
"context"
"crypto/x509"
"flag"
"fmt"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/grafana/authlib/authn"
"github.com/grafana/grafana-app-sdk/logging"
"github.com/urfave/cli/v2"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/transport"
authrt "github.com/grafana/grafana/apps/provisioning/pkg/auth"
"github.com/grafana/grafana/apps/provisioning/pkg/controller"
client "github.com/grafana/grafana/apps/provisioning/pkg/generated/clientset/versioned"
informer "github.com/grafana/grafana/apps/provisioning/pkg/generated/informers/externalversions"
)
var (
token = flag.String("token", "", "Token to use for authentication")
tokenExchangeURL = flag.String("token-exchange-url", "", "Token exchange URL")
provisioningServerURL = flag.String("provisioning-server-url", "", "Provisioning server URL")
tlsInsecure = flag.Bool("tls-insecure", true, "Skip TLS certificate verification")
tlsCertFile = flag.String("tls-cert-file", "", "Path to TLS certificate file")
tlsKeyFile = flag.String("tls-key-file", "", "Path to TLS private key file")
tlsCAFile = flag.String("tls-ca-file", "", "Path to TLS CA certificate file")
)
func main() {
app := &cli.App{
Name: "job-controller",
Usage: "Watch provisioning jobs and manage job history cleanup",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "token",
Usage: "Token to use for authentication",
Value: "",
Destination: token,
},
&cli.StringFlag{
Name: "token-exchange-url",
Usage: "Token exchange URL",
Value: "",
Destination: tokenExchangeURL,
},
&cli.StringFlag{
Name: "provisioning-server-url",
Usage: "Provisioning server URL",
Value: "",
Destination: provisioningServerURL,
},
&cli.BoolFlag{
Name: "tls-insecure",
Usage: "Skip TLS certificate verification",
Value: true,
Destination: tlsInsecure,
},
&cli.StringFlag{
Name: "tls-cert-file",
Usage: "Path to TLS certificate file",
Value: "",
Destination: tlsCertFile,
},
&cli.StringFlag{
Name: "tls-key-file",
Usage: "Path to TLS private key file",
Value: "",
Destination: tlsKeyFile,
},
&cli.StringFlag{
Name: "tls-ca-file",
Usage: "Path to TLS CA certificate file",
Value: "",
Destination: tlsCAFile,
},
&cli.DurationFlag{
Name: "history-expiration",
Usage: "Duration after which HistoricJobs are deleted; 0 disables cleanup. When the Provisioning API is configured to use Loki for job history, leave this at 0.",
Value: 0,
},
},
Action: runJobController,
}
if err := app.Run(os.Args); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
func runJobController(c *cli.Context) error {
// TODO: Wire notifications into a ConcurrentJobDriver when a client-backed Store and Workers are available.
// For now, just log notifications to verify events end-to-end.
logger := logging.NewSLogLogger(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
})).With("logger", "provisioning-job-controller")
logger.Info("Starting provisioning job controller")
tokenExchangeClient, err := authn.NewTokenExchangeClient(authn.TokenExchangeConfig{
TokenExchangeURL: *tokenExchangeURL,
Token: *token,
})
if err != nil {
return fmt.Errorf("failed to create token exchange client: %w", err)
}
tlsConfig, err := buildTLSConfig()
if err != nil {
return fmt.Errorf("failed to build TLS configuration: %w", err)
}
config := &rest.Config{
APIPath: "/apis",
Host: *provisioningServerURL,
WrapTransport: transport.WrapperFunc(func(rt http.RoundTripper) http.RoundTripper {
return authrt.NewRoundTripper(tokenExchangeClient, rt)
}),
TLSClientConfig: tlsConfig,
}
provisioningClient, err := client.NewForConfig(config)
if err != nil {
return fmt.Errorf("failed to create provisioning client: %w", err)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigChan
fmt.Println("Received shutdown signal, stopping controllers")
cancel()
}()
// Jobs informer and controller (resync ~60s like in register.go)
jobInformerFactory := informer.NewSharedInformerFactoryWithOptions(
provisioningClient,
60*time.Second,
)
jobInformer := jobInformerFactory.Provisioning().V0alpha1().Jobs()
jobController, err := controller.NewJobController(jobInformer)
if err != nil {
return fmt.Errorf("failed to create job controller: %w", err)
}
logger.Info("jobs controller started")
notifications := jobController.InsertNotifications()
go func() {
for {
select {
case <-ctx.Done():
return
case <-notifications:
logger.Info("job create notification received")
}
}
}()
// Optionally enable history cleanup if a positive expiration is provided
historyExpiration := c.Duration("history-expiration")
var startHistoryInformers func()
if historyExpiration > 0 {
// History jobs informer and controller (separate factory with resync == expiration)
historyInformerFactory := informer.NewSharedInformerFactoryWithOptions(
provisioningClient,
historyExpiration,
)
historyJobInformer := historyInformerFactory.Provisioning().V0alpha1().HistoricJobs()
_, err = controller.NewHistoryJobController(
provisioningClient.ProvisioningV0alpha1(),
historyJobInformer,
historyExpiration,
)
if err != nil {
return fmt.Errorf("failed to create history job controller: %w", err)
}
logger.Info("history cleanup enabled", "expiration", historyExpiration.String())
startHistoryInformers = func() { historyInformerFactory.Start(ctx.Done()) }
} else {
startHistoryInformers = func() {}
}
// Start informers
go jobInformerFactory.Start(ctx.Done())
go startHistoryInformers()
// Optionally wait for job cache sync; history cleanup can rely on resync events
if !cache.WaitForCacheSync(ctx.Done(), jobInformer.Informer().HasSynced) {
return fmt.Errorf("failed to sync job informer cache")
}
<-ctx.Done()
return nil
}
func buildTLSConfig() (rest.TLSClientConfig, error) {
tlsConfig := rest.TLSClientConfig{
Insecure: *tlsInsecure,
}
// If client certificate and key are provided
if *tlsCertFile != "" && *tlsKeyFile != "" {
tlsConfig.CertFile = *tlsCertFile
tlsConfig.KeyFile = *tlsKeyFile
}
// If CA certificate is provided
if *tlsCAFile != "" {
caCert, err := os.ReadFile(*tlsCAFile)
if err != nil {
return tlsConfig, fmt.Errorf("failed to read CA certificate file: %w", err)
}
caCertPool := x509.NewCertPool()
if !caCertPool.AppendCertsFromPEM(caCert) {
return tlsConfig, fmt.Errorf("failed to parse CA certificate")
}
tlsConfig.CAData = caCert
}
return tlsConfig, nil
}
+31 -1
View File
@@ -3,18 +3,29 @@ module github.com/grafana/grafana/apps/provisioning
go 1.24.6
require (
github.com/grafana/authlib v0.0.0-20250710201142-9542f2f28d43
github.com/grafana/grafana-app-sdk/logging v0.40.3
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2
github.com/stretchr/testify v1.10.0
github.com/urfave/cli/v2 v2.27.7
k8s.io/apimachinery v0.33.3
k8s.io/apiserver v0.33.3
k8s.io/client-go v0.33.3
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff
sigs.k8s.io/structured-merge-diff/v4 v4.7.0
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/emicklei/go-restful/v3 v3.12.1 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-jose/go-jose/v3 v3.0.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
@@ -23,6 +34,8 @@ require (
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grafana/authlib/types v0.0.0-20250710201142-9542f2f28d43 // indirect
github.com/grafana/dskit v0.0.0-20250611075409-46f51e1ce914 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
@@ -31,24 +44,41 @@ require (
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/onsi/ginkgo/v2 v2.22.2 // indirect
github.com/onsi/gomega v1.36.2 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.23.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.65.0 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/spf13/pflag v1.0.7 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.10.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel v1.37.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/otel/sdk v1.37.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/crypto v0.41.0 // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/term v0.34.0 // indirect
golang.org/x/text v0.28.0 // indirect
golang.org/x/time v0.11.0 // indirect
golang.org/x/tools v0.36.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
google.golang.org/grpc v1.74.2 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.33.3 // indirect
k8s.io/component-base v0.33.3 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
+98
View File
@@ -1,3 +1,11 @@
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
@@ -6,8 +14,13 @@ github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtz
github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
@@ -18,6 +31,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw=
github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
@@ -28,6 +43,14 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grafana/authlib v0.0.0-20250710201142-9542f2f28d43 h1:vVPT0i5Y1vI6qzecYStV2yk7cHKrC3Pc7AgvwT5KydQ=
github.com/grafana/authlib v0.0.0-20250710201142-9542f2f28d43/go.mod h1:1fWkOiL+m32NBgRHZtlZGz2ji868tPZACYbqP3nBRJI=
github.com/grafana/authlib/types v0.0.0-20250710201142-9542f2f28d43 h1:NlkGMnVi/oUn6Cr90QbJYpQJ4FnjyAIG9Ex5GtTZIzw=
github.com/grafana/authlib/types v0.0.0-20250710201142-9542f2f28d43/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw=
github.com/grafana/dskit v0.0.0-20250611075409-46f51e1ce914 h1:qcSGhr691f1mmPHwg2svGyO40Ex92G02aOyHzP6XHCE=
github.com/grafana/dskit v0.0.0-20250611075409-46f51e1ce914/go.mod h1:OiN4P4aC6LwLzLbEupH3Ue83VfQoNMfG48rsna8jI/E=
github.com/grafana/grafana-app-sdk/logging v0.40.3 h1:2VXsXXEQiqAavRP8wusRDB6rDqf5lufP7A6NfjELqPE=
github.com/grafana/grafana-app-sdk/logging v0.40.3/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU=
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 h1:X0cnaFdR+iz+sDSuoZmkryFSjOirchHe2MdKSRwBWgM=
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2/go.mod h1:RRvSjHH12/PnQaXraMO65jUhVu8n59mzvhfIMBETnV4=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
@@ -36,10 +59,14 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -53,25 +80,55 @@ github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU
github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk=
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
@@ -81,12 +138,22 @@ go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
@@ -94,15 +161,35 @@ golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKl
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
@@ -111,12 +198,18 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -126,14 +219,19 @@ gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSP
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/api v0.33.3 h1:SRd5t//hhkI1buzxb288fy2xvjubstenEKL9K51KBI8=
k8s.io/api v0.33.3/go.mod h1:01Y/iLUjNBM3TAvypct7DIj0M0NIZc+PzAHCIo0CYGE=
k8s.io/apimachinery v0.33.3 h1:4ZSrmNa0c/ZpZJhAgRdcsFcZOw1PQU1bALVQ0B3I5LA=
k8s.io/apimachinery v0.33.3/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
k8s.io/apiserver v0.33.3 h1:Wv0hGc+QFdMJB4ZSiHrCgN3zL3QRatu56+rpccKC3J4=
k8s.io/apiserver v0.33.3/go.mod h1:05632ifFEe6TxwjdAIrwINHWE2hLwyADFk5mBsQa15E=
k8s.io/client-go v0.33.3 h1:M5AfDnKfYmVJif92ngN532gFqakcGi6RvaOF16efrpA=
k8s.io/client-go v0.33.3/go.mod h1:luqKBQggEf3shbxHY4uVENAxrDISLOarxpTKMiUuujg=
k8s.io/component-base v0.33.3 h1:mlAuyJqyPlKZM7FyaoM/LcunZaaY353RXiOd2+B5tGA=
k8s.io/component-base v0.33.3/go.mod h1:ktBVsBzkI3imDuxYXmVxZ2zxJnYTZ4HAsVj9iF09qp4=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4=
@@ -0,0 +1,45 @@
package auth
import (
"context"
"fmt"
"net/http"
"github.com/grafana/authlib/authn"
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
utilnet "k8s.io/apimachinery/pkg/util/net"
)
// tokenExchanger abstracts the token exchange client for testability.
type tokenExchanger interface {
Exchange(ctx context.Context, req authn.TokenExchangeRequest) (*authn.TokenExchangeResponse, error)
}
// RoundTripper injects an exchanged access token for the provisioning API into outgoing requests.
type RoundTripper struct {
client tokenExchanger
transport http.RoundTripper
}
// NewRoundTripper constructs a RoundTripper that exchanges the provided token per request
// and forwards the request to the provided base transport.
func NewRoundTripper(tokenExchangeClient tokenExchanger, base http.RoundTripper) *RoundTripper {
return &RoundTripper{
client: tokenExchangeClient,
transport: base,
}
}
func (t *RoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
tokenResponse, err := t.client.Exchange(req.Context(), authn.TokenExchangeRequest{
Audiences: []string{provisioning.GROUP},
Namespace: "*",
})
if err != nil {
return nil, fmt.Errorf("failed to exchange token: %w", err)
}
req = utilnet.CloneRequest(req)
req.Header.Set("X-Access-Token", "Bearer "+tokenResponse.Token)
return t.transport.RoundTrip(req)
}
@@ -0,0 +1,63 @@
package auth
import (
"context"
"io"
"net/http"
"net/http/httptest"
"testing"
"github.com/grafana/authlib/authn"
)
type fakeExchanger struct {
resp *authn.TokenExchangeResponse
err error
}
func (f *fakeExchanger) Exchange(_ context.Context, req authn.TokenExchangeRequest) (*authn.TokenExchangeResponse, error) {
return f.resp, f.err
}
// roundTripperFunc allows building a stub transport inline
type roundTripperFunc func(*http.Request) (*http.Response, error)
func (f roundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) }
func TestRoundTripper_SetsAccessTokenHeader(t *testing.T) {
tr := NewRoundTripper(&fakeExchanger{resp: &authn.TokenExchangeResponse{Token: "abc123"}}, roundTripperFunc(func(r *http.Request) (*http.Response, error) {
got := r.Header.Get("X-Access-Token")
if got != "Bearer abc123" {
t.Fatalf("expected X-Access-Token header 'Bearer abc123', got %q", got)
}
// Return a minimal response; body must be non-nil per http.RoundTripper contract
rr := httptest.NewRecorder()
rr.WriteHeader(http.StatusOK)
return rr.Result(), nil
}))
req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://example", nil)
resp, err := tr.RoundTrip(req)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// drain and close body
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}
func TestRoundTripper_PropagatesExchangeError(t *testing.T) {
tr := NewRoundTripper(&fakeExchanger{err: io.EOF}, roundTripperFunc(func(_ *http.Request) (*http.Response, error) {
t.Fatal("transport should not be called on exchange error")
return nil, nil
}))
req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://example", nil)
resp, err := tr.RoundTrip(req)
if err == nil {
if resp != nil && resp.Body != nil {
_ = resp.Body.Close()
}
t.Fatalf("expected error, got nil")
}
}
+1 -1
View File
@@ -28,7 +28,7 @@ require (
github.com/go-test/deep v1.1.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/gnostic-models v0.6.9 // indirect
github.com/grafana/grafana-app-sdk/logging v0.40.2 // indirect
github.com/grafana/grafana-app-sdk/logging v0.40.3 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/josharian/intern v1.0.0 // indirect
+2 -2
View File
@@ -38,8 +38,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grafana/grafana-app-sdk v0.40.3 h1:JFo7uAfbAJUfZ9neD7/4sODKm1xgu9zhckclH/N4DYU=
github.com/grafana/grafana-app-sdk v0.40.3/go.mod h1:j0KzHo3Sa6kd+lnwSScBNoV9Vobkg/YY9HtEjxpyPrk=
github.com/grafana/grafana-app-sdk/logging v0.40.2 h1:HQ1+y9Od92iMbWWB54QxiYpNtCvYGUVpyxvxZ7ywB1k=
github.com/grafana/grafana-app-sdk/logging v0.40.2/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU=
github.com/grafana/grafana-app-sdk/logging v0.40.3 h1:2VXsXXEQiqAavRP8wusRDB6rDqf5lufP7A6NfjELqPE=
github.com/grafana/grafana-app-sdk/logging v0.40.3/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU=
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250710134100-1f3dc0533caf h1:BBGDHffvVNLoYQlXEpbXcxE0vbpq7pm/8OWF5I+UDZg=
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250710134100-1f3dc0533caf/go.mod h1:eAlOam2uWhrsEZlOoAr7XZ9hbBP7SyYGYn31/aQAPs8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+1 -1
View File
@@ -31,7 +31,7 @@ require (
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grafana/grafana-app-sdk/logging v0.40.2 // indirect
github.com/grafana/grafana-app-sdk/logging v0.40.3 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
+2 -2
View File
@@ -49,8 +49,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grafana/grafana-app-sdk v0.40.3 h1:JFo7uAfbAJUfZ9neD7/4sODKm1xgu9zhckclH/N4DYU=
github.com/grafana/grafana-app-sdk v0.40.3/go.mod h1:j0KzHo3Sa6kd+lnwSScBNoV9Vobkg/YY9HtEjxpyPrk=
github.com/grafana/grafana-app-sdk/logging v0.40.2 h1:HQ1+y9Od92iMbWWB54QxiYpNtCvYGUVpyxvxZ7ywB1k=
github.com/grafana/grafana-app-sdk/logging v0.40.2/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU=
github.com/grafana/grafana-app-sdk/logging v0.40.3 h1:2VXsXXEQiqAavRP8wusRDB6rDqf5lufP7A6NfjELqPE=
github.com/grafana/grafana-app-sdk/logging v0.40.3/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -434,9 +434,6 @@
"id": 1,
"options": {
"cellHeight": "sm",
"frozenColumns": {
"left": 1
},
"footer": {
"countRows": false,
"enablePagination": false,
@@ -445,6 +442,9 @@
"show": true
},
"frameIndex": 0,
"frozenColumns": {
"left": 1
},
"showHeader": true,
"sortBy": [
{
@@ -797,6 +797,10 @@
"value": {
"type": "data-links"
}
},
{
"id": "custom.width",
"value": 140
}
]
}
@@ -804,7 +808,7 @@
},
"gridPos": {
"h": 7,
"w": 24,
"w": 12,
"x": 0,
"y": 12
},
@@ -834,6 +838,166 @@
"title": "Colors and Links",
"type": "table"
},
{
"datasource": {
"type": "grafana-testdata-datasource"
},
"fieldConfig": {
"defaults": {
"color": {
"fixedColor": "transparent",
"mode": "fixed"
},
"custom": {
"align": "auto",
"cellOptions": {
"type": "auto"
},
"inspect": false,
"width": 100,
"wrapHeaderText": false
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
}
]
}
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "highlight"
},
"properties": [
{
"id": "custom.cellOptions",
"value": {
"applyToRow": true,
"mode": "basic",
"type": "color-background"
}
},
{
"id": "mappings",
"value": [
{
"options": {
"1": {
"color": "#fff899",
"index": 0
}
},
"type": "value"
}
]
}
]
},
{
"matcher": {
"id": "byRegexp",
"options": "/^color-*/"
},
"properties": [
{
"id": "mappings",
"value": [
{
"options": {
"1": {
"color": "green",
"index": 0
}
},
"type": "value"
}
]
}
]
},
{
"matcher": {
"id": "byName",
"options": "color-bg"
},
"properties": [
{
"id": "custom.cellOptions",
"value": {
"type": "color-background"
}
}
]
},
{
"matcher": {
"id": "byName",
"options": "color-text"
},
"properties": [
{
"id": "custom.cellOptions",
"value": {
"type": "color-text"
}
},
{
"id": "color",
"value": {
"fixedColor": "text",
"mode": "fixed"
}
}
]
},
{
"matcher": {
"id": "byName",
"options": "use-case"
},
"properties": [
{
"id": "custom.width"
}
]
}
]
},
"gridPos": {
"h": 7,
"w": 12,
"x": 12,
"y": 12
},
"id": 10,
"options": {
"cellHeight": "sm",
"footer": {
"countRows": false,
"fields": "",
"reducer": ["sum"],
"show": false
},
"showHeader": true,
"sortBy": []
},
"pluginVersion": "12.2.0-pre",
"targets": [
{
"csvContent": "use-case,normal,color-text,color-bg,highlight\n\"color bg and apply to row\",1,0,1,1\n\"color text and apply to row\",1,1,0,1\n\"color text + color bg + apply to row\",1,1,1,1\n\"only apply to row\",1,0,0,1\n\"only color bg\",1,0,1,0\n\"only color text\",1,1,0,0\n\"color text + color bg\",1,1,1,0\n\"no colorization\",1,0,0,0",
"refId": "A",
"scenarioId": "csv_content"
}
],
"title": "Apply to Row - mixed color cell types",
"type": "table"
},
{
"datasource": {
"type": "grafana-testdata-datasource"
@@ -1697,5 +1861,5 @@
"timezone": "",
"title": "Panel Tests - Table - Kitchen Sink",
"uid": "dcb9f5e9-8066-4397-889e-864b99555dbb",
"version": 9
"version": 32
}
@@ -57,6 +57,7 @@
"type": "linear"
},
"showPoints": "auto",
"showValues": true,
"spanNulls": false,
"stacking": {
"group": "A",
@@ -79,20 +79,18 @@ Folders help you organize and group dashboards, which is useful when you have ma
- On the **Dashboards** page, click **New** and select **New folder** in the drop-down.
- Click an existing folder and on the folder’s page, click **New** and select **New folder** in the drop-down.
1. Enter a unique name and click **Create**.
1. Enter a unique name.
Folder names can't include underscores (\_) or percentage signs (%), as it interferes with the search functionality.
Also, alerts can't be placed in folders with slashes (\ /) in the name. If you want to place alerts in the folder, don't use slashes in the folder name.
1. Click **Create**
When you nest folders, you can do so up to four levels deep.
When you save a dashboard, you can optionally select a folder to save the dashboard in.
{{< admonition type="note" >}}
{{< /admonition >}}
**To edit the name of a folder:**
1. Click **Dashboards** in the primary menu.
@@ -482,6 +482,8 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
}
```
Note that the UID cannot be modified.
**Example Response**:
```http
@@ -547,7 +549,7 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
{
"id":1,
"uid": "updated UID",
"uid": "uid",
"orgId":1,
"name":"test_datasource",
"type":"graphite",
@@ -566,6 +568,8 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
}
```
Note that the UID cannot be modified.
**Example Response**:
```http
@@ -575,7 +579,7 @@ Content-Type: application/json
{
"datasource": {
"id": 1,
"uid": "updated UID",
"uid": "uid",
"orgId": 1,
"name": "test_datasource",
"type": "graphite",
+3 -3
View File
@@ -96,7 +96,7 @@ require (
github.com/grafana/gomemcache v0.0.0-20250318131618-74242eea118d // @grafana/grafana-operator-experience-squad
github.com/grafana/grafana-api-golang-client v0.27.0 // @grafana/alerting-backend
github.com/grafana/grafana-app-sdk v0.40.3 // @grafana/grafana-app-platform-squad
github.com/grafana/grafana-app-sdk/logging v0.40.2 // @grafana/grafana-app-platform-squad
github.com/grafana/grafana-app-sdk/logging v0.40.3 // @grafana/grafana-app-platform-squad
github.com/grafana/grafana-aws-sdk v1.1.0 // @grafana/aws-datasources
github.com/grafana/grafana-azure-sdk-go/v2 v2.2.0 // @grafana/partner-datasources
github.com/grafana/grafana-cloud-migration-snapshot v1.9.0 // @grafana/grafana-operator-experience-squad
@@ -172,7 +172,7 @@ require (
github.com/tjhop/slog-gokit v0.1.3 // @grafana/grafana-app-platform-squad
github.com/ua-parser/uap-go v0.0.0-20250213224047-9c035f085b90 // @grafana/grafana-backend-group
github.com/urfave/cli v1.22.16 // indirect; @grafana/grafana-backend-group
github.com/urfave/cli/v2 v2.27.6 // @grafana/grafana-backend-group
github.com/urfave/cli/v2 v2.27.7 // @grafana/grafana-backend-group
github.com/urfave/cli/v3 v3.3.3 // @grafana/grafana-backend-group
github.com/wk8/go-ordered-map v1.0.0 // @grafana/grafana-backend-group
github.com/xlab/treeprint v1.2.0 // @grafana/observability-traces-and-profiling
@@ -377,7 +377,7 @@ require (
github.com/cockroachdb/apd/v3 v3.2.1 // indirect
github.com/coreos/go-semver v0.3.1 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dennwc/varint v1.0.0 // indirect
github.com/dgraph-io/ristretto/v2 v2.2.0 // indirect
+6 -5
View File
@@ -1054,8 +1054,9 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cznic/b v0.0.0-20180115125044-35e9bbe41f07/go.mod h1:URriBxXwVq5ijiJ12C7iIZqlA69nTlI+LgI6/pwftG8=
github.com/cznic/fileutil v0.0.0-20180108211300-6a051e75936f/go.mod h1:8S58EK26zhXSxzv7NQFpnliaOQsmDUxvoQO3rt154Vg=
@@ -1602,8 +1603,8 @@ github.com/grafana/grafana-api-golang-client v0.27.0 h1:zIwMXcbCB4n588i3O2N6HfNc
github.com/grafana/grafana-api-golang-client v0.27.0/go.mod h1:uNLZEmgKtTjHBtCQMwNn3qsx2mpMb8zU+7T4Xv3NR9Y=
github.com/grafana/grafana-app-sdk v0.40.3 h1:JFo7uAfbAJUfZ9neD7/4sODKm1xgu9zhckclH/N4DYU=
github.com/grafana/grafana-app-sdk v0.40.3/go.mod h1:j0KzHo3Sa6kd+lnwSScBNoV9Vobkg/YY9HtEjxpyPrk=
github.com/grafana/grafana-app-sdk/logging v0.40.2 h1:HQ1+y9Od92iMbWWB54QxiYpNtCvYGUVpyxvxZ7ywB1k=
github.com/grafana/grafana-app-sdk/logging v0.40.2/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU=
github.com/grafana/grafana-app-sdk/logging v0.40.3 h1:2VXsXXEQiqAavRP8wusRDB6rDqf5lufP7A6NfjELqPE=
github.com/grafana/grafana-app-sdk/logging v0.40.3/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU=
github.com/grafana/grafana-aws-sdk v1.1.0 h1:G0fvwbQmHw14c5RXPd7Gnw9ZQcgzl139LtMDoe0KhmE=
github.com/grafana/grafana-aws-sdk v1.1.0/go.mod h1:7e+47EdHynteYWGoT5Ere9KeOXQObsk8F0vkOLQ1tz8=
github.com/grafana/grafana-azure-sdk-go/v2 v2.2.0 h1:0TYrkzAc3u0HX+9GK86cGrLTUAcmQfl3/LEB3tL+SOA=
@@ -2447,8 +2448,8 @@ github.com/unknwon/log v0.0.0-20200308114134-929b1006e34a/go.mod h1:1xEUf2abjfP9
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ=
github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po=
github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g=
github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
github.com/urfave/cli/v3 v3.3.3 h1:byCBaVdIXuLPIDm5CYZRVG6NvT7tv1ECqdU4YzlEa3I=
github.com/urfave/cli/v3 v3.3.3/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
+1 -2
View File
@@ -1003,8 +1003,7 @@ github.com/grafana/grafana-app-sdk/logging v0.39.0 h1:3GgN5+dUZYqq74Q+GT9/ET+yo+
github.com/grafana/grafana-app-sdk/logging v0.39.0/go.mod h1:WhDENSnaGHtyVVwZGVnAR7YLvh2xlLDYR3D7E6h7XVk=
github.com/grafana/grafana-app-sdk/logging v0.39.1/go.mod h1:WhDENSnaGHtyVVwZGVnAR7YLvh2xlLDYR3D7E6h7XVk=
github.com/grafana/grafana-app-sdk/logging v0.40.0/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU=
github.com/grafana/grafana-app-sdk/logging v0.40.3 h1:2VXsXXEQiqAavRP8wusRDB6rDqf5lufP7A6NfjELqPE=
github.com/grafana/grafana-app-sdk/logging v0.40.3/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU=
github.com/grafana/grafana-app-sdk/logging v0.40.2/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU=
github.com/grafana/grafana-app-sdk/plugin v0.41.0 h1:ShUvGpAVzM3UxcsfwS6l/lwW4ytDeTbCQXf8w2P8Yp8=
github.com/grafana/grafana-app-sdk/plugin v0.41.0/go.mod h1:YIhimVfAqtOp3kdhxOanaSZjypVKh/bYxf9wfFfhDm0=
github.com/grafana/grafana-aws-sdk v0.38.2 h1:TzQD0OpWsNjtldi5G5TLDlBRk8OyDf+B5ujcoAu4Dp0=
+220 -215
View File
@@ -3,7 +3,10 @@
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"datasource": {
"type": "datasource",
"uid": "grafana"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
@@ -18,15 +21,16 @@
}
]
},
"editable": true,
"gnetId": null,
"editable": false,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": 3085,
"iteration": 1631554945276,
"id": 23,
"links": [],
"panels": [
{
"datasource": "$datasource",
"datasource": {
"uid": "$datasource"
},
"fieldConfig": {
"defaults": {
"mappings": [],
@@ -35,8 +39,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -59,31 +62,37 @@
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": ["mean"],
"fields": "",
"values": false
},
"showPercentChange": false,
"text": {},
"textMode": "auto"
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "8.1.3",
"pluginVersion": "12.0.2",
"targets": [
{
"datasource": {
"uid": "$datasource"
},
"expr": "grafana_alerting_result_total{job=~\"$job\", instance=~\"$instance\", state=\"alerting\"}",
"instant": true,
"interval": "",
"interval": "1m",
"legendFormat": "",
"refId": "A"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Firing Alerts",
"type": "stat"
},
{
"datasource": "$datasource",
"datasource": {
"uid": "$datasource"
},
"fieldConfig": {
"defaults": {
"mappings": [],
@@ -91,8 +100,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -115,43 +123,50 @@
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": ["mean"],
"fields": "",
"values": false
},
"showPercentChange": false,
"text": {},
"textMode": "auto"
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "8.1.3",
"pluginVersion": "12.0.2",
"targets": [
{
"datasource": {
"uid": "$datasource"
},
"expr": "sum(grafana_stat_totals_dashboard{job=~\"$job\", instance=~\"$instance\"})",
"interval": "",
"interval": "1m",
"legendFormat": "",
"refId": "A"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Dashboards",
"type": "stat"
},
{
"datasource": "$datasource",
"datasource": {
"uid": "$datasource"
},
"fieldConfig": {
"defaults": {
"custom": {
"align": null,
"displayMode": "auto"
"cellOptions": {
"type": "auto"
},
"inspect": false
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -170,26 +185,38 @@
},
"id": 10,
"options": {
"cellHeight": "sm",
"footer": {
"countRows": false,
"fields": "",
"reducer": ["sum"],
"show": false
},
"showHeader": true
},
"pluginVersion": "8.1.3",
"pluginVersion": "12.0.2",
"targets": [
{
"datasource": {
"uid": "$datasource"
},
"expr": "grafana_build_info{job=~\"$job\", instance=~\"$instance\"}",
"instant": true,
"interval": "",
"interval": "1m",
"legendFormat": "",
"refId": "A"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Build Info",
"transformations": [
{
"id": "labelsToFields",
"options": {}
},
{
"id": "merge",
"options": {}
},
{
"id": "organize",
"options": {
@@ -224,254 +251,247 @@
"type": "table"
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "$datasource",
"datasource": {
"uid": "$datasource"
},
"fieldConfig": {
"defaults": {
"links": []
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "normal"
},
"thresholdsStyle": {
"mode": "off"
}
},
"links": [],
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
},
"unit": "reqps"
},
"overrides": []
},
"fill": 1,
"fillGradient": 0,
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 5
},
"hiddenSeries": false,
"id": 2,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"nullPointMode": "null",
"options": {
"alertThreshold": true
"alertThreshold": true,
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"hideZeros": false,
"mode": "multi",
"sort": "none"
}
},
"percentage": false,
"pluginVersion": "8.1.3",
"pointradius": 2,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": true,
"steppedLine": false,
"pluginVersion": "12.0.2",
"targets": [
{
"datasource": {
"uid": "$datasource"
},
"expr": "sum by (status_code) (irate(grafana_http_request_duration_seconds_count{job=~\"$job\", instance=~\"$instance\"}[1m])) ",
"interval": "",
"interval": "1m",
"legendFormat": "{{status_code}}",
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeRegions": [],
"timeShift": null,
"title": "RPS",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"$$hashKey": "object:157",
"format": "reqps",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"$$hashKey": "object:158",
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": false
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
"type": "timeseries"
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "$datasource",
"datasource": {
"uid": "$datasource"
},
"fieldConfig": {
"defaults": {
"links": []
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"links": [],
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
},
"unit": "ms"
},
"overrides": []
},
"fill": 1,
"fillGradient": 0,
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 5
},
"hiddenSeries": false,
"id": 4,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"nullPointMode": "null",
"options": {
"alertThreshold": true
"alertThreshold": true,
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"hideZeros": false,
"mode": "multi",
"sort": "none"
}
},
"percentage": false,
"pluginVersion": "8.1.3",
"pointradius": 2,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"pluginVersion": "12.0.2",
"targets": [
{
"datasource": {
"uid": "$datasource"
},
"exemplar": true,
"expr": "histogram_quantile(0.99, sum(irate(grafana_http_request_duration_seconds_bucket{instance=~\"$instance\", job=~\"$job\"}[$__rate_interval])) by (le)) * 1",
"interval": "",
"interval": "1m",
"legendFormat": "99th Percentile",
"refId": "A"
},
{
"datasource": {
"uid": "$datasource"
},
"exemplar": true,
"expr": "histogram_quantile(0.50, sum(irate(grafana_http_request_duration_seconds_bucket{instance=~\"$instance\", job=~\"$job\"}[$__rate_interval])) by (le)) * 1",
"interval": "",
"interval": "1m",
"legendFormat": "50th Percentile",
"refId": "B"
},
{
"datasource": {
"uid": "$datasource"
},
"exemplar": true,
"expr": "sum(irate(grafana_http_request_duration_seconds_sum{instance=~\"$instance\", job=~\"$job\"}[$__rate_interval])) * 1 / sum(irate(grafana_http_request_duration_seconds_count{instance=~\"$instance\", job=~\"$job\"}[$__rate_interval]))",
"interval": "",
"interval": "1m",
"legendFormat": "Average",
"refId": "C"
}
],
"thresholds": [],
"timeFrom": null,
"timeRegions": [],
"timeShift": null,
"title": "Request Latency",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"$$hashKey": "object:210",
"format": "ms",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"$$hashKey": "object:211",
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
"type": "timeseries"
}
],
"schemaVersion": 30,
"preload": false,
"refresh": "",
"schemaVersion": 41,
"tags": [],
"templating": {
"list": [
{
"current": {
"selected": true,
"text": "dev-cortex",
"value": "dev-cortex"
"text": "Prometheus",
"value": "prometheus"
},
"description": null,
"error": null,
"hide": 0,
"includeAll": false,
"label": null,
"multi": false,
"name": "datasource",
"options": [],
"query": "prometheus",
"queryValue": "",
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"type": "datasource"
},
{
"allValue": ".*",
"current": {
"selected": false,
"text": ["default/grafana"],
"value": ["default/grafana"]
"text": "All",
"value": ["$__all"]
},
"datasource": "$datasource",
"definition": "label_values(grafana_build_info, job)",
"description": null,
"error": null,
"hide": 0,
"includeAll": true,
"label": null,
"multi": true,
"name": "job",
"options": [],
@@ -481,27 +501,17 @@
},
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 0,
"tagValuesQuery": "",
"tagsQuery": "",
"type": "query",
"useTags": false
"type": "query"
},
{
"allValue": ".*",
"current": {
"selected": false,
"text": "All",
"value": "$__all"
},
"datasource": "$datasource",
"definition": "label_values(grafana_build_info, instance)",
"description": null,
"error": null,
"hide": 0,
"includeAll": true,
"label": null,
"multi": true,
"name": "instance",
"options": [],
@@ -511,12 +521,7 @@
},
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 0,
"tagValuesQuery": "",
"tagsQuery": "",
"type": "query",
"useTags": false
"type": "query"
}
]
},
@@ -527,8 +532,8 @@
"timepicker": {
"refresh_intervals": ["10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"]
},
"timezone": "",
"timezone": "utc",
"title": "Grafana Overview",
"uid": "6be0s85Mk",
"version": 2
"version": 1
}
+2 -2
View File
@@ -291,8 +291,8 @@
"@grafana/plugin-ui": "0.10.9",
"@grafana/prometheus": "workspace:*",
"@grafana/runtime": "workspace:*",
"@grafana/scenes": "^6.30.0",
"@grafana/scenes-react": "^6.30.0",
"@grafana/scenes": "6.30.4",
"@grafana/scenes-react": "6.30.4",
"@grafana/schema": "workspace:*",
"@grafana/sql": "workspace:*",
"@grafana/ui": "workspace:*",
@@ -635,6 +635,7 @@ export interface GraphFieldConfig extends LineConfig, FillConfig, PointsConfig,
drawStyle?: GraphDrawStyle;
gradientMode?: GraphGradientMode;
insertNulls?: (boolean | number);
showValues?: boolean;
thresholdsStyle?: GraphThresholdsStyleConfig;
transform?: GraphTransform;
}
@@ -229,6 +229,7 @@ GraphFieldConfig: {
gradientMode?: GraphGradientMode
thresholdsStyle?: GraphThresholdsStyleConfig
transform?: GraphTransform
showValues?: bool
insertNulls?: bool | number
} @cuetsy(kind="interface")
@@ -54,6 +54,10 @@ export interface TempoQuery extends common.DataQuery {
* Filters to be included in a PromQL query to select data for the service graph. Example: {client="app",service="app"}. Providing multiple values will produce union of results for each filter, using PromQL OR operator internally.
*/
serviceMapQuery?: (string | Array<string>);
/**
* Whether to use native histograms for service map queries
*/
serviceMapUseNativeHistograms?: boolean;
/**
* @deprecated Query traces by service name
*/
@@ -6,18 +6,16 @@ export interface ConversionStatus {
// If true, means that the dashboard is not valid,
// and the caller should instead fetch the stored version.
failed: boolean;
// The version which was stored when the dashboard was created / updated.
// Fetching this version should always succeed.
storedVersion: string;
// The error message from the conversion.
// Empty if the conversion has not failed.
error: string;
error?: string;
// The version which was stored when the dashboard was created / updated.
// Fetching this version should always succeed.
storedVersion?: string;
}
export const defaultConversionStatus = (): ConversionStatus => ({
failed: false,
storedVersion: "",
error: "",
});
export interface Status {
@@ -6,18 +6,16 @@ export interface ConversionStatus {
// If true, means that the dashboard is not valid,
// and the caller should instead fetch the stored version.
failed: boolean;
// The version which was stored when the dashboard was created / updated.
// Fetching this version should always succeed.
storedVersion: string;
// The error message from the conversion.
// Empty if the conversion has not failed.
error: string;
error?: string;
// The version which was stored when the dashboard was created / updated.
// Fetching this version should always succeed.
storedVersion?: string;
}
export const defaultConversionStatus = (): ConversionStatus => ({
failed: false,
storedVersion: "",
error: "",
});
export interface Status {
@@ -6,18 +6,16 @@ export interface ConversionStatus {
// If true, means that the dashboard is not valid,
// and the caller should instead fetch the stored version.
failed: boolean;
// The version which was stored when the dashboard was created / updated.
// Fetching this version should always succeed.
storedVersion: string;
// The error message from the conversion.
// Empty if the conversion has not failed.
error: string;
error?: string;
// The version which was stored when the dashboard was created / updated.
// Fetching this version should always succeed.
storedVersion?: string;
}
export const defaultConversionStatus = (): ConversionStatus => ({
failed: false,
storedVersion: "",
error: "",
});
export interface Status {
@@ -6,18 +6,16 @@ export interface ConversionStatus {
// If true, means that the dashboard is not valid,
// and the caller should instead fetch the stored version.
failed: boolean;
// The version which was stored when the dashboard was created / updated.
// Fetching this version should always succeed.
storedVersion: string;
// The error message from the conversion.
// Empty if the conversion has not failed.
error: string;
error?: string;
// The version which was stored when the dashboard was created / updated.
// Fetching this version should always succeed.
storedVersion?: string;
}
export const defaultConversionStatus = (): ConversionStatus => ({
failed: false,
storedVersion: "",
error: "",
});
export interface Status {
@@ -2,6 +2,8 @@ import { render, RenderResult } from '@testing-library/react';
import { Field, FieldType, MappingType, createTheme } from '@grafana/data';
import { getTextColorForBackground } from '../../../../utils/colors';
import { PillCell } from './PillCell';
describe('PillCell', () => {
@@ -26,49 +28,94 @@ describe('PillCell', () => {
describe('Color by hash (classic palette)', () => {
it('single value', () => {
expectHTML(
render(<PillCell field={fieldWithValues(['value1'])} rowIdx={0} theme={theme} />),
`<span style="background-color: rgb(63, 43, 91); color: rgb(255, 255, 255);">value1</span>`
render(
<PillCell
getTextColorForBackground={getTextColorForBackground}
field={fieldWithValues(['value1'])}
rowIdx={0}
theme={theme}
/>
),
`<span style="background-color: rgb(63, 43, 91); color: rgb(247, 248, 250);">value1</span>`
);
});
it('empty string', () => {
expectHTML(render(<PillCell field={fieldWithValues([''])} rowIdx={0} theme={theme} />), '');
expectHTML(
render(
<PillCell
getTextColorForBackground={getTextColorForBackground}
field={fieldWithValues([''])}
rowIdx={0}
theme={theme}
/>
),
''
);
});
it('null', () => {
const { container } = render(<PillCell field={fieldWithValues([])} rowIdx={0} theme={theme} />);
const { container } = render(
<PillCell
getTextColorForBackground={getTextColorForBackground}
field={fieldWithValues([])}
rowIdx={0}
theme={theme}
/>
);
expect(container).toBeEmptyDOMElement();
});
it('CSV values', () => {
expectHTML(
render(<PillCell field={fieldWithValues(['value1,value2,value3'])} rowIdx={0} theme={theme} />),
render(
<PillCell
getTextColorForBackground={getTextColorForBackground}
field={fieldWithValues(['value1,value2,value3'])}
rowIdx={0}
theme={theme}
/>
),
`
<span style="background-color: rgb(63, 43, 91); color: rgb(255, 255, 255);">value1</span>
<span style="background-color: rgb(252, 226, 222); color: rgb(0, 0, 0);">value2</span>
<span style="background-color: rgb(81, 149, 206); color: rgb(0, 0, 0);">value3</span>
<span style="background-color: rgb(63, 43, 91); color: rgb(247, 248, 250);">value1</span>
<span style="background-color: rgb(252, 226, 222); color: rgb(32, 34, 38);">value2</span>
<span style="background-color: rgb(81, 149, 206); color: rgb(247, 248, 250);">value3</span>
`
);
});
it('JSON array values', () => {
expectHTML(
render(<PillCell field={fieldWithValues(['["value1","value2","value3"]'])} rowIdx={0} theme={theme} />),
render(
<PillCell
getTextColorForBackground={getTextColorForBackground}
field={fieldWithValues(['["value1","value2","value3"]'])}
rowIdx={0}
theme={theme}
/>
),
`
<span style="background-color: rgb(63, 43, 91); color: rgb(255, 255, 255);">value1</span>
<span style="background-color: rgb(252, 226, 222); color: rgb(0, 0, 0);">value2</span>
<span style="background-color: rgb(81, 149, 206); color: rgb(0, 0, 0);">value3</span>
<span style="background-color: rgb(63, 43, 91); color: rgb(247, 248, 250);">value1</span>
<span style="background-color: rgb(252, 226, 222); color: rgb(32, 34, 38);">value2</span>
<span style="background-color: rgb(81, 149, 206); color: rgb(247, 248, 250);">value3</span>
`
);
});
it('non-string values', () => {
expectHTML(
render(<PillCell field={fieldWithValues(['[100,200,300]'])} rowIdx={0} theme={theme} />),
render(
<PillCell
getTextColorForBackground={getTextColorForBackground}
field={fieldWithValues(['[100,200,300]'])}
rowIdx={0}
theme={theme}
/>
),
`
<span style="background-color: rgb(252, 226, 222); color: rgb(0, 0, 0);">100</span>
<span style="background-color: rgb(222, 218, 247); color: rgb(0, 0, 0);">200</span>
<span style="background-color: rgb(249, 217, 249); color: rgb(0, 0, 0);">300</span>
<span style="background-color: rgb(252, 226, 222); color: rgb(32, 34, 38);">100</span>
<span style="background-color: rgb(222, 218, 247); color: rgb(32, 34, 38);">200</span>
<span style="background-color: rgb(249, 217, 249); color: rgb(32, 34, 38);">300</span>
`
);
});
@@ -107,12 +154,14 @@ describe('PillCell', () => {
} satisfies Field;
expectHTML(
render(<PillCell field={field} rowIdx={0} theme={theme} />),
render(
<PillCell getTextColorForBackground={getTextColorForBackground} field={field} rowIdx={0} theme={theme} />
),
`
<span style="background-color: rgb(0, 255, 0); color: rgb(0, 0, 0);">success</span>
<span style="background-color: rgb(255, 0, 0); color: rgb(0, 0, 0);">error</span>
<span style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">warning</span>
<span style="background-color: rgb(255, 120, 10); color: rgb(0, 0, 0);">unknown</span>
<span style="background-color: rgb(0, 255, 0); color: rgb(247, 248, 250);">success</span>
<span style="background-color: rgb(255, 0, 0); color: rgb(247, 248, 250);">error</span>
<span style="background-color: rgb(255, 255, 0); color: rgb(32, 34, 38);">warning</span>
<span style="background-color: rgb(255, 120, 10); color: rgb(247, 248, 250);">unknown</span>
`
);
});
@@ -4,7 +4,6 @@ import { useMemo } from 'react';
import {
GrafanaTheme2,
classicColors,
colorManipulator,
Field,
getColorByStringHash,
FALLBACK_COLOR,
@@ -14,12 +13,23 @@ import { FieldColorModeId } from '@grafana/schema';
import { PillCellProps, TableCellStyles, TableCellValue } from '../types';
export function PillCell({ rowIdx, field, theme }: PillCellProps) {
export function PillCell({ rowIdx, field, theme, getTextColorForBackground }: PillCellProps) {
const value = field.values[rowIdx];
const pills: Pill[] = useMemo(() => {
const pillValues = inferPills(value);
return pillValues.length > 0 ? createPills(pillValues, field, theme) : [];
}, [value, field, theme]);
return pillValues.length > 0
? pillValues.map((pill, index) => {
const bgColor = getPillColor(pill, field, theme);
const textColor = getTextColorForBackground(bgColor);
return {
value: String(pill),
key: `${pill}-${index}`,
bgColor,
color: textColor,
};
})
: [];
}, [value, field, theme, getTextColorForBackground]);
if (pills.length === 0) {
return null;
@@ -49,19 +59,6 @@ interface Pill {
const SPLIT_RE = /\s*,\s*/;
const TRANSPARENT = 'rgba(0,0,0,0)';
function createPills(pillValues: unknown[], field: Field, theme: GrafanaTheme2): Pill[] {
return pillValues.map((pill, index) => {
const bgColor = getPillColor(pill, field, theme);
const textColor = colorManipulator.getContrastRatio('#FFFFFF', bgColor) >= 4.5 ? '#FFFFFF' : '#000000';
return {
value: String(pill),
key: `${pill}-${index}`,
bgColor,
color: textColor,
};
});
}
export function inferPills(rawValue: TableCellValue): unknown[] {
if (rawValue === '' || rawValue == null) {
return [];
@@ -91,6 +91,7 @@ describe('TableNG Cells renderers', () => {
getActions={jest.fn(() => [
{ title: 'Action', onClick: jest.fn(() => {}), confirmation: jest.fn(), style: {} },
])}
getTextColorForBackground={jest.fn(() => '#000000')}
/>
);
};
@@ -109,6 +110,7 @@ describe('TableNG Cells renderers', () => {
height={100}
width={100}
theme={createTheme()}
getTextColorForBackground={jest.fn(() => '#000000')}
cellOptions={cellOptions}
cellInspect={false}
showFilters={false}
@@ -110,7 +110,12 @@ const CELL_REGISTRY: Record<TableCellOptions['type'], CellRegistryEntry> = {
[TableCellDisplayMode.Pill]: {
// eslint-disable-next-line react/display-name
renderer: memo((props: TableCellRendererProps) => (
<PillCell rowIdx={props.rowIdx} field={props.field} theme={props.theme} />
<PillCell
rowIdx={props.rowIdx}
field={props.field}
theme={props.theme}
getTextColorForBackground={props.getTextColorForBackground}
/>
)),
getStyles: getPillStyles,
testField: (field: Field) => field.type === FieldType.string,
@@ -1,6 +1,7 @@
import 'react-data-grid/lib/styles.css';
import { clsx } from 'clsx';
import memoize from 'micro-memoize';
import { CSSProperties, Key, ReactNode, useCallback, useMemo, useRef, useState } from 'react';
import {
Cell,
@@ -28,6 +29,7 @@ import { Trans } from '@grafana/i18n';
import { FieldColorModeId, TableCellTooltipPlacement } from '@grafana/schema';
import { useStyles2, useTheme2 } from '../../../themes/ThemeContext';
import { getTextColorForBackground as _getTextColorForBackground } from '../../../utils/colors';
import { Pagination } from '../../Pagination/Pagination';
import { PanelContext, usePanelContext } from '../../PanelChrome';
import { DataLinksActionsTooltip } from '../DataLinksActionsTooltip';
@@ -80,7 +82,7 @@ import {
frameToRecords,
getAlignment,
getApplyToRowBgFn,
getCellColorInlineStyles,
getCellColorInlineStylesFactory,
getCellLinks,
getCellOptions,
getDefaultRowHeight,
@@ -143,6 +145,7 @@ export function TableNG(props: TableNGProps) {
const rows = useMemo(() => frameToRecords(data), [data]);
const hasNestedFrames = useMemo(() => getIsNestedTable(data.fields), [data]);
const getTextColorForBackground = useMemo(() => memoize(_getTextColorForBackground, { maxSize: 1000 }), []);
const {
rows: filteredRows,
@@ -174,6 +177,11 @@ export function TableNG(props: TableNGProps) {
() => (hasNestedFrames ? width - COLUMN.EXPANDER_WIDTH : width) - scrollbarWidth,
[width, hasNestedFrames, scrollbarWidth]
);
const getCellColorInlineStyles = useMemo(() => getCellColorInlineStylesFactory(theme), [theme]);
const applyToRowBgFn = useMemo(
() => getApplyToRowBgFn(data.fields, getCellColorInlineStyles) ?? undefined,
[data.fields, getCellColorInlineStyles]
);
const typographyCtx = useMemo(
() =>
createTypographyContext(
@@ -226,7 +234,6 @@ export function TableNG(props: TableNGProps) {
footerOptions,
isCountRowsSet,
});
const applyToRowBgFn = useMemo(() => getApplyToRowBgFn(data.fields, theme) ?? undefined, [data.fields, theme]);
// normalize the row height into a function which returns a number, so we avoid a bunch of conditionals during rendering.
const rowHeightFn = useMemo((): ((row: TableRow) => number) => {
@@ -438,14 +445,12 @@ export function TableNG(props: TableNGProps) {
}
}
let style: CSSProperties | undefined;
if (rowCellStyle.color != null || rowCellStyle.background != null) {
style = rowCellStyle;
} else if (canBeColorized) {
let style: CSSProperties = { ...rowCellStyle };
if (canBeColorized) {
const value = props.row[props.column.key];
const displayValue = field.display!(value); // this fires here to get colors, then again to get rendered value?
style = getCellColorInlineStyles(theme, cellOptions, displayValue);
const cellColorStyles = getCellColorInlineStyles(cellOptions, displayValue, applyToRowBgFn != null);
Object.assign(style, cellColorStyles);
}
return (
@@ -486,6 +491,7 @@ export function TableNG(props: TableNGProps) {
showFilters={showFilters}
getActions={getCellActions}
disableSanitizeHtml={disableSanitizeHtml}
getTextColorForBackground={getTextColorForBackground}
/>
{showActions && (
<TableCellActions
@@ -549,6 +555,7 @@ export function TableNG(props: TableNGProps) {
disableSanitizeHtml,
field: tooltipField,
getActions: getCellActions,
getTextColorForBackground,
gridRef,
placement,
renderer: tooltipFieldRenderer,
@@ -560,10 +567,15 @@ export function TableNG(props: TableNGProps) {
renderCellContent = (props: RenderCellProps<TableRow, TableSummaryRow>): JSX.Element => {
// cached so we don't care about multiple calls.
const tooltipHeight = rowHeightFn(props.row);
let tooltipStyle: CSSProperties | undefined;
let tooltipStyle: CSSProperties = { ...rowCellStyle };
if (tooltipCanBeColorized) {
const tooltipDisplayValue = tooltipField.display!(props.row[tooltipDisplayName]); // this is yet another call to field.display() for the tooltip field
tooltipStyle = getCellColorInlineStyles(theme, tooltipCellOptions, tooltipDisplayValue);
const tooltipDisplayValue = tooltipField.display!(props.row[tooltipDisplayName]);
const tooltipCellColorStyles = getCellColorInlineStyles(
tooltipCellOptions,
tooltipDisplayValue,
applyToRowBgFn != null
);
Object.assign(tooltipStyle, tooltipCellColorStyles);
}
return (
@@ -624,6 +636,8 @@ export function TableNG(props: TableNGProps) {
footerCalcs,
frozenColumns,
getCellActions,
getCellColorInlineStyles,
getTextColorForBackground,
isCountRowsSet,
numFrozenColsFullyInView,
onCellFilterAdded,
@@ -19,6 +19,7 @@ export interface TableCellTooltipProps {
disableSanitizeHtml?: boolean;
field: Field;
getActions: (field: Field, rowIdx: number) => ActionModel[];
getTextColorForBackground: (bgColor: string) => string;
gridRef: RefObject<DataGridHandle>;
height: number;
placement?: TableCellTooltipPlacement;
@@ -40,6 +41,7 @@ export const TableCellTooltip = memo(
disableSanitizeHtml,
field,
getActions,
getTextColorForBackground,
gridRef,
height,
placement,
@@ -100,6 +102,7 @@ export const TableCellTooltip = memo(
field,
frame: data,
getActions,
getTextColorForBackground,
height,
rowIdx,
showFilters: false,
@@ -107,7 +110,19 @@ export const TableCellTooltip = memo(
value: rawValue,
width,
}) satisfies TableCellRendererProps,
[cellOptions, data, disableSanitizeHtml, field, getActions, height, rawValue, rowIdx, theme, width]
[
cellOptions,
data,
disableSanitizeHtml,
field,
getActions,
getTextColorForBackground,
height,
rawValue,
rowIdx,
theme,
width,
]
);
const cellElement = tooltipCaretRef.current?.closest<HTMLElement>('.rdg-cell');
@@ -167,6 +167,7 @@ export interface TableCellRendererProps {
showFilters: boolean;
getActions?: GetActionsFunctionLocal;
disableSanitizeHtml?: boolean;
getTextColorForBackground: (color: string) => string;
}
export type InspectCellProps = {
@@ -250,6 +251,7 @@ export interface PillCellProps {
theme: GrafanaTheme2;
field: Field;
rowIdx: number;
getTextColorForBackground: (color: string) => string;
}
export interface TableCellStyleOptions {
@@ -23,7 +23,7 @@ import {
extractPixelValue,
frameToRecords,
getAlignmentFactor,
getCellColorInlineStyles,
getCellColorInlineStylesFactory,
getCellLinks,
getCellOptions,
getComparator,
@@ -107,28 +107,104 @@ describe('TableNG utils', () => {
},
} as unknown as GrafanaTheme2;
it('should handle color background mode', () => {
const field = { type: TableCellDisplayMode.ColorBackground as const, mode: TableCellBackgroundDisplayMode.Basic };
it('should handle color text cell type', () => {
const cellOptions = {
type: TableCellDisplayMode.ColorText as const,
};
const displayValue = { text: '100', numeric: 100, color: '#ff0000' };
const colors = getCellColorInlineStyles(theme, field, displayValue);
expect(colors.background).toBe('rgb(255, 0, 0)');
const getCellColorInlineStyles = getCellColorInlineStylesFactory(theme);
const colors = getCellColorInlineStyles(cellOptions, displayValue, false);
expect(colors.color).toBe('#ff0000');
expect(colors).not.toHaveProperty('background');
});
it('should pass thru color background cell type in basic mode', () => {
const cellOptions = {
type: TableCellDisplayMode.ColorBackground as const,
mode: TableCellBackgroundDisplayMode.Basic,
};
const displayValue = { text: '100', numeric: 100, color: '#ff0000' };
const getCellColorInlineStyles = getCellColorInlineStylesFactory(theme);
const colors = getCellColorInlineStyles(cellOptions, displayValue, false);
expect(colors.background).toBe('#ff0000');
expect(colors.color).toBe('rgb(247, 248, 250)');
});
it('should handle color background gradient mode', () => {
const field = {
it('should handle color background cell type in gradient mode', () => {
const cellOptions = {
type: TableCellDisplayMode.ColorBackground as const,
mode: TableCellBackgroundDisplayMode.Gradient,
};
const displayValue = { text: '100', numeric: 100, color: '#ff0000' };
const colors = getCellColorInlineStyles(theme, field, displayValue);
const getCellColorInlineStyles = getCellColorInlineStylesFactory(theme);
const colors = getCellColorInlineStyles(cellOptions, displayValue, false);
expect(colors.background).toBe('linear-gradient(120deg, rgb(255, 54, 36), #ff0000)');
expect(colors.color).toBe('rgb(247, 248, 250)');
});
it('does not set CSSProperties for un-mapped cell types', () => {
const cellOptions = { type: TableCellDisplayMode.JSONView as const };
const displayValue = { text: '100', numeric: 100, color: '#ff0000' };
const getCellColorInlineStyles = getCellColorInlineStylesFactory(theme);
const colors = getCellColorInlineStyles(cellOptions, displayValue, false);
expect(colors).toEqual({});
});
describe('applyToRow', () => {
it.each([
['hex', '#ffffff00'],
['rgba', 'rgba(255,255,255,0)'],
['hsla', 'hsla(0,100%,100%,0)'],
])(
'should not apply background color if the display value is transparent (%s) and applyToRow is on',
(_format, colorDisplayValue) => {
const cellOptions = {
type: TableCellDisplayMode.ColorBackground as const,
mode: TableCellBackgroundDisplayMode.Basic,
};
const displayValue = { text: '100', numeric: 100, color: colorDisplayValue };
const getCellColorInlineStyles = getCellColorInlineStylesFactory(theme);
const colors = getCellColorInlineStyles(cellOptions, displayValue, true);
expect(colors).toEqual({});
}
);
it.each([
['hex', '#ffffff00'],
['rgba', 'rgba(255,255,255,0)'],
['hsla', 'hsla(0,100%,100%,0)'],
])(
'should apply background color if the display value is transparent (%s) and applyToRow is off',
(_format, colorDisplayValue) => {
const cellOptions = {
type: TableCellDisplayMode.ColorBackground as const,
mode: TableCellBackgroundDisplayMode.Basic,
};
const displayValue = { text: '100', numeric: 100, color: colorDisplayValue };
const getCellColorInlineStyles = getCellColorInlineStylesFactory(theme);
const colors = getCellColorInlineStyles(cellOptions, displayValue, false);
expect(colors).toEqual({
background: colorDisplayValue,
color: 'rgb(32, 34, 38)',
});
}
);
});
});
describe('frame to records conversion', () => {
@@ -1,4 +1,5 @@
import { Property } from 'csstype';
import memoize from 'micro-memoize';
import { CSSProperties } from 'react';
import { SortColumn } from 'react-data-grid';
import tinycolor from 'tinycolor2';
@@ -499,36 +500,58 @@ const CELL_GRADIENT_HUE_ROTATION_DEGREES = 5;
* @internal
* Returns the text and background colors for a table cell based on its options and display value.
*/
export function getCellColorInlineStyles(
theme: GrafanaTheme2,
cellOptions: TableCellOptions,
displayValue: DisplayValue
): CSSProperties {
// How much to darken elements depends upon if we're in dark mode
const darkeningFactor = theme.isDark ? 1 : -0.7;
// Setup color variables
let textColor: string | undefined = undefined;
let bgColor: string | undefined = undefined;
if (cellOptions.type === TableCellDisplayMode.ColorText) {
textColor = displayValue.color;
} else if (cellOptions.type === TableCellDisplayMode.ColorBackground) {
const mode = cellOptions.mode ?? TableCellBackgroundDisplayMode.Gradient;
if (mode === TableCellBackgroundDisplayMode.Basic) {
textColor = getTextColorForAlphaBackground(displayValue.color!, theme.isDark);
bgColor = tinycolor(displayValue.color).toRgbString();
} else if (mode === TableCellBackgroundDisplayMode.Gradient) {
const bgColor2 = tinycolor(displayValue.color)
export function getCellColorInlineStylesFactory(theme: GrafanaTheme2) {
const bgCellTextColor = memoize((color: string) => getTextColorForAlphaBackground(color, theme.isDark), {
maxSize: 1000,
});
const darkeningFactor = theme.isDark ? 1 : -0.7; // How much to darken elements depends upon if we're in dark mode
const gradientBg = memoize(
(color: string) =>
tinycolor(color)
.darken(CELL_COLOR_DARKENING_MULTIPLIER * darkeningFactor)
.spin(CELL_GRADIENT_HUE_ROTATION_DEGREES);
textColor = getTextColorForAlphaBackground(displayValue.color!, theme.isDark);
bgColor = `linear-gradient(120deg, ${bgColor2.toRgbString()}, ${displayValue.color})`;
}
}
.spin(CELL_GRADIENT_HUE_ROTATION_DEGREES)
.toRgbString(),
{ maxSize: 1000 }
);
const isTransparent = memoize(
(color: string) => {
// if hex, do the simple thing.
if (color[0] === '#') {
return color.length === 9 && color.endsWith('00');
}
// if not hex, just use tinycolor to avoid extra logic.
return tinycolor(color).getAlpha() === 0;
},
{ maxSize: 1000 }
);
return { color: textColor, background: bgColor };
return (cellOptions: TableCellOptions, displayValue: DisplayValue, hasApplyToRow: boolean): CSSProperties => {
const result: CSSProperties = {};
const displayValueColor = displayValue.color;
if (!displayValueColor) {
return result;
}
if (cellOptions.type === TableCellDisplayMode.ColorText) {
result.color = displayValueColor;
} else if (cellOptions.type === TableCellDisplayMode.ColorBackground) {
// return without setting anything if the bg is transparent. this allows
// the cell to inherit the row bg color if `applyToRow` is set.
if (hasApplyToRow && isTransparent(displayValueColor)) {
return result;
}
const mode = cellOptions.mode ?? TableCellBackgroundDisplayMode.Gradient;
result.color = bgCellTextColor(displayValueColor);
result.background =
mode === TableCellBackgroundDisplayMode.Gradient
? `linear-gradient(120deg, ${gradientBg(displayValueColor)}, ${displayValueColor})`
: displayValueColor;
}
return result;
};
}
/**
@@ -881,7 +904,10 @@ export function computeColWidths(fields: Field[], availWidth: number) {
* @internal
* if applyToRow is true in any field, return a function that gets the row background color
*/
export function getApplyToRowBgFn(fields: Field[], theme: GrafanaTheme2): ((rowIndex: number) => CSSProperties) | void {
export function getApplyToRowBgFn(
fields: Field[],
getCellColorInlineStyles: ReturnType<typeof getCellColorInlineStylesFactory>
): ((rowIndex: number) => CSSProperties) | void {
for (const field of fields) {
const cellOptions = getCellOptions(field);
const fieldDisplay = field.display;
@@ -890,7 +916,7 @@ export function getApplyToRowBgFn(fields: Field[], theme: GrafanaTheme2): ((rowI
cellOptions.type === TableCellDisplayMode.ColorBackground &&
cellOptions.applyToRow === true
) {
return (rowIndex: number) => getCellColorInlineStyles(theme, cellOptions, fieldDisplay(field.values[rowIndex]));
return (rowIndex: number) => getCellColorInlineStyles(cellOptions, fieldDisplay(field.values[rowIndex]), true);
}
}
}
@@ -49,6 +49,7 @@ export interface SeriesProps extends LineConfig, BarConfig, FillConfig, PointsCo
dataFrameFieldIndex?: DataFrameFieldIndex;
theme: GrafanaTheme2;
value?: uPlot.Series.Value;
showValues?: boolean;
}
export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> {
+1 -1
View File
@@ -34,7 +34,7 @@ require (
github.com/chromedp/cdproto v0.0.0-20250429231605-6ed5b53462d4 // indirect
github.com/coreos/go-semver v0.3.1 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/elazarl/goproxy v1.7.2 // indirect
github.com/evanphx/json-patch v5.9.11+incompatible // indirect
+2 -1
View File
@@ -40,8 +40,9 @@ github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+1 -1
View File
@@ -5,7 +5,7 @@ go 1.24.6
require (
github.com/google/go-cmp v0.7.0
github.com/grafana/authlib/types v0.0.0-20250710201142-9542f2f28d43
github.com/grafana/grafana-app-sdk/logging v0.40.2
github.com/grafana/grafana-app-sdk/logging v0.40.3
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e
github.com/prometheus/client_golang v1.23.0
github.com/stretchr/testify v1.10.0
+2 -2
View File
@@ -84,8 +84,8 @@ github.com/grafana/authlib/types v0.0.0-20250710201142-9542f2f28d43 h1:NlkGMnVi/
github.com/grafana/authlib/types v0.0.0-20250710201142-9542f2f28d43/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw=
github.com/grafana/dskit v0.0.0-20250611075409-46f51e1ce914 h1:qcSGhr691f1mmPHwg2svGyO40Ex92G02aOyHzP6XHCE=
github.com/grafana/dskit v0.0.0-20250611075409-46f51e1ce914/go.mod h1:OiN4P4aC6LwLzLbEupH3Ue83VfQoNMfG48rsna8jI/E=
github.com/grafana/grafana-app-sdk/logging v0.40.2 h1:HQ1+y9Od92iMbWWB54QxiYpNtCvYGUVpyxvxZ7ywB1k=
github.com/grafana/grafana-app-sdk/logging v0.40.2/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU=
github.com/grafana/grafana-app-sdk/logging v0.40.3 h1:2VXsXXEQiqAavRP8wusRDB6rDqf5lufP7A6NfjELqPE=
github.com/grafana/grafana-app-sdk/logging v0.40.3/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU=
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e h1:BTKk7LHuG1kmAkucwTA7DuMbKpKvJTKrGdBmUNO4dfQ=
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e/go.mod h1:IA4SOwun8QyST9c5UNs/fN37XL6boXXDvRYFcFwbipg=
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI=
+2 -2
View File
@@ -9,7 +9,7 @@ replace github.com/docker/docker => github.com/moby/moby v27.5.1+incompatible
require (
github.com/google/uuid v1.6.0 // indirect; @grafana/grafana-backend-group
github.com/urfave/cli/v2 v2.27.6 // @grafana/grafana-backend-group
github.com/urfave/cli/v2 v2.27.7 // @grafana/grafana-backend-group
go.opentelemetry.io/otel v1.37.0 // indirect; @grafana/grafana-backend-group
go.opentelemetry.io/otel/sdk v1.37.0 // indirect; @grafana/grafana-backend-group
go.opentelemetry.io/otel/trace v1.37.0 // indirect; @grafana/grafana-backend-group
@@ -21,7 +21,7 @@ require (
)
require (
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/go-logr/logr v1.4.3 // indirect; @grafana/grafana-app-platform-squad
github.com/go-logr/stdr v1.2.2 // indirect

Some files were not shown because too many files have changed in this diff Show More