Merge branch 'main' into leeoniya/state-timeline-value-mappings

This commit is contained in:
Leon Sorokin
2025-02-27 17:59:39 -06:00
177 changed files with 5458 additions and 1247 deletions
+8 -32
View File
@@ -285,6 +285,9 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
],
"packages/grafana-data/src/types/scopes.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"packages/grafana-data/src/types/select.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
@@ -1491,9 +1494,6 @@ exports[`better eslint`] = {
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "3"]
],
"public/app/features/alerting/unified/Templates.test.tsx:5381": [
[0, 0, 0, "\'@grafana/ui/src/components/Monaco/types\' import is restricted from being used by a pattern. Import from the public export instead.", "0"]
],
"public/app/features/alerting/unified/components/AlertLabel.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
],
@@ -1728,9 +1728,6 @@ exports[`better eslint`] = {
"public/app/features/alerting/unified/components/extensions/AlertInstanceExtensionPoint.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
],
"public/app/features/alerting/unified/components/extensions/AlertInstanceExtensionPointMenu.tsx:5381": [
[0, 0, 0, "Do not re-export imported variable (\`app/features/explore/extensions/ToolbarExtensionPointMenu\`)", "0"]
],
"public/app/features/alerting/unified/components/mute-timings/MuteTimingActionsButtons.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"]
@@ -1901,9 +1898,6 @@ exports[`better eslint`] = {
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "11"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "12"]
],
"public/app/features/alerting/unified/components/receivers/TemplateEditor.tsx:5381": [
[0, 0, 0, "\'@grafana/ui/src/components/Monaco/types\' import is restricted from being used by a pattern. Import from the public export instead.", "0"]
],
"public/app/features/alerting/unified/components/receivers/TemplateForm.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"],
@@ -1988,10 +1982,8 @@ exports[`better eslint`] = {
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "6"]
],
"public/app/features/alerting/unified/components/receivers/form/fields/KeyValueMapInput.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "3"]
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"]
],
"public/app/features/alerting/unified/components/receivers/form/fields/OptionField.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
@@ -2022,9 +2014,6 @@ exports[`better eslint`] = {
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"]
],
"public/app/features/alerting/unified/components/receivers/form/fields/TemplateSelector.test.tsx:5381": [
[0, 0, 0, "\'@grafana/ui/src/components/Monaco/types\' import is restricted from being used by a pattern. Import from the public export instead.", "0"]
],
"public/app/features/alerting/unified/components/receivers/form/fields/TemplateSelector.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"],
@@ -2298,14 +2287,13 @@ exports[`better eslint`] = {
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "8"]
],
"public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx:5381": [
[0, 0, 0, "\'@grafana/ui/src/components/Text/Text\' import is restricted from being used by a pattern. Import from the public export instead.", "0"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "2"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "3"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "4"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "5"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "6"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "7"]
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "6"]
],
"public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/SimpleCondition.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
@@ -2371,12 +2359,6 @@ exports[`better eslint`] = {
"public/app/features/alerting/unified/components/rule-viewer/tabs/Routing.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
],
"public/app/features/alerting/unified/components/rules/ActionButton.tsx:5381": [
[0, 0, 0, "\'@grafana/ui/src/components/Button\' import is restricted from being used by a pattern. Import from the public export instead.", "0"]
],
"public/app/features/alerting/unified/components/rules/ActionIcon.tsx:5381": [
[0, 0, 0, "\'@grafana/ui/src/components/Tooltip\' import is restricted from being used by a pattern. Import from the public export instead.", "0"]
],
"public/app/features/alerting/unified/components/rules/AlertInstanceDetails.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
],
@@ -2814,15 +2796,9 @@ exports[`better eslint`] = {
"public/app/features/alerting/unified/rule-list/components/RuleGroup.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
],
"public/app/features/alerting/unified/rule-list/components/RuleListIcon.tsx:5381": [
[0, 0, 0, "\'@grafana/ui/src/components/Text/Text\' import is restricted from being used by a pattern. Import from the public export instead.", "0"]
],
"public/app/features/alerting/unified/state/AlertingQueryRunner.test.ts:5381": [
[0, 0, 0, "\'@grafana/runtime/src/utils/DataSourceWithBackend\' import is restricted from being used by a pattern. Import from the public export instead.", "0"]
],
"public/app/features/alerting/unified/state/actions.ts:5381": [
[0, 0, 0, "\'@grafana/runtime/src/utils/logging\' import is restricted from being used by a pattern. Import from the public export instead.", "0"]
],
"public/app/features/alerting/unified/types/receiver-form.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
+2
View File
@@ -776,6 +776,7 @@ embed.go @grafana/grafana-as-code
/.github/workflows/doc-validator.yml @grafana/docs-tooling
/.github/workflows/deploy-pr-preview.yml @grafana/docs-tooling
/.github/workflows/epic-add-to-platform-ux-parent-project.yml @meanmina
/.github/workflows/feature-toggles-ci.yml @grafana/docs-tooling
/.github/workflows/github-release.yml @grafana/grafana-developer-enablement-squad
/.github/workflows/issue-opened.yml @grafana/grafana-community-support
/.github/workflows/metrics-collector.yml @torkelo
@@ -812,6 +813,7 @@ embed.go @grafana/grafana-as-code
/.github/workflows/trivy-scan.yml @grafana/grafana-backend-services-squad
/.github/workflows/changelog.yml @zserge
/.github/workflows/actions/changelog @zserge
/.github/workflows/frontend-unit-tests.yml @grafana/grafana-frontend-platform
# Generated files not requiring owner approval
/packages/grafana-data/src/types/featureToggles.gen.ts @grafanabot
+1
View File
@@ -32,6 +32,7 @@
extends: ["schedule:monthly"],
groupName: "Storybook updates",
matchPackageNames: ["/^@?storybook/"],
rangeStrategy: "bump",
},
{
groupName: "React Aria",
+25
View File
@@ -0,0 +1,25 @@
name: Feature toggles CI
on:
pull_request:
paths:
- 'pkg/services/featuremgmt/toggles_gen_test.go'
- 'pkg/services/featuremgmt/registry.go'
- 'docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md'
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: 'go.mod'
cache: true
- name: Run feature toggle tests
run: go test -v -run TestFeatureToggleFiles ./pkg/services/featuremgmt/
+29
View File
@@ -0,0 +1,29 @@
name: Frontend tests
on:
pull_request:
push:
branches:
- main
- release-*.*.*
jobs:
test-frontend:
runs-on: ubuntu-latest-8-cores
name: "Unit tests (${{ matrix.chunk }} / 8)"
strategy:
fail-fast: true
matrix:
chunk: [1, 2, 3, 4, 5, 6, 7, 8]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
cache-dependency-path: 'yarn.lock'
- run: yarn install --immutable --check-cache
- run: yarn run test:ci
env:
TEST_MAX_WORKERS: 2
TEST_SHARD: ${{ matrix.chunk }}
TEST_SHARD_TOTAL: 8
+2 -2
View File
@@ -32,7 +32,7 @@ require (
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect
github.com/google/uuid v1.6.0 // indirect
@@ -83,7 +83,7 @@ require (
google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
google.golang.org/grpc v1.70.0 // indirect
google.golang.org/protobuf v1.36.4 // indirect
google.golang.org/protobuf v1.36.5 // 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
+4 -4
View File
@@ -60,8 +60,8 @@ github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl76
github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -269,8 +269,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.
google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+2 -2
View File
@@ -27,7 +27,7 @@ require (
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect
github.com/google/uuid v1.6.0 // indirect
@@ -73,7 +73,7 @@ require (
google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
google.golang.org/grpc v1.70.0 // indirect
google.golang.org/protobuf v1.36.4 // indirect
google.golang.org/protobuf v1.36.5 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.32.1 // indirect
+4 -4
View File
@@ -40,8 +40,8 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6
github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -195,8 +195,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+2 -2
View File
@@ -28,7 +28,7 @@ require (
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect
github.com/google/uuid v1.6.0 // indirect
@@ -74,7 +74,7 @@ require (
google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
google.golang.org/grpc v1.70.0 // indirect
google.golang.org/protobuf v1.36.4 // indirect
google.golang.org/protobuf v1.36.5 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.32.1 // indirect
+4 -4
View File
@@ -40,8 +40,8 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6
github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -195,8 +195,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+4 -4
View File
@@ -65,7 +65,7 @@ require (
github.com/golang/mock v1.7.0-rc.1 // @grafana/alerting-backend
github.com/golang/protobuf v1.5.4 // @grafana/grafana-backend-group
github.com/golang/snappy v0.0.4 // @grafana/alerting-backend
github.com/google/go-cmp v0.6.0 // @grafana/grafana-backend-group
github.com/google/go-cmp v0.7.0 // @grafana/grafana-backend-group
github.com/google/go-querystring v1.1.0 // indirect; @grafana/oss-big-tent
github.com/google/uuid v1.6.0 // @grafana/grafana-backend-group
github.com/google/wire v0.6.0 // @grafana/grafana-backend-group
@@ -88,7 +88,7 @@ require (
github.com/grafana/grafana-cloud-migration-snapshot v1.6.0 // @grafana/grafana-operator-experience-squad
github.com/grafana/grafana-google-sdk-go v0.2.1 // @grafana/partner-datasources
github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 // @grafana/grafana-backend-group
github.com/grafana/grafana-plugin-sdk-go v0.266.0 // @grafana/plugins-platform-backend
github.com/grafana/grafana-plugin-sdk-go v0.267.0 // @grafana/plugins-platform-backend
github.com/grafana/loki/v3 v3.2.1 // @grafana/observability-logs
github.com/grafana/otel-profiling-go v0.5.1 // @grafana/grafana-backend-group
github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // @grafana/observability-traces-and-profiling
@@ -180,7 +180,7 @@ require (
gonum.org/v1/gonum v0.15.1 // @grafana/oss-big-tent
google.golang.org/api v0.216.0 // @grafana/grafana-backend-group
google.golang.org/grpc v1.70.0 // @grafana/plugins-platform-backend
google.golang.org/protobuf v1.36.4 // @grafana/plugins-platform-backend
google.golang.org/protobuf v1.36.5 // @grafana/plugins-platform-backend
gopkg.in/ini.v1 v1.67.0 // @grafana/alerting-backend
gopkg.in/mail.v2 v2.3.1 // @grafana/grafana-backend-group
gopkg.in/yaml.v3 v3.0.1 // @grafana/alerting-backend
@@ -331,7 +331,7 @@ require (
github.com/dolthub/maphash v0.1.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/edsrzf/mmap-go v1.2.0 // indirect
github.com/elazarl/goproxy v1.7.0 // indirect
github.com/elazarl/goproxy v1.7.1 // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/emicklei/proto v1.13.2 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
+8 -7
View File
@@ -1094,8 +1094,8 @@ github.com/edsrzf/mmap-go v1.2.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8E
github.com/efficientgo/core v1.0.0-rc.3 h1:X6CdgycYWDcbYiJr1H1+lQGzx13o7bq3EUkbB9DsSPc=
github.com/efficientgo/core v1.0.0-rc.3/go.mod h1:FfGdkzWarkuzOlY04VY+bGfb1lWrjaL6x/GLcQ4vJps=
github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
github.com/elazarl/goproxy v1.7.0 h1:EXv2nV4EjM60ZtsEVLYJG4oBXhDGutMKperpHsZ/v+0=
github.com/elazarl/goproxy v1.7.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ=
github.com/elazarl/goproxy v1.7.1 h1:1P7LPSxbqtNxusFnXclj6O56pjfq1xOQZ6a0mwwKUlY=
github.com/elazarl/goproxy v1.7.1/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
@@ -1422,8 +1422,9 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
@@ -1558,8 +1559,8 @@ github.com/grafana/grafana-google-sdk-go v0.2.1 h1:XeFdKnkXBjOJjXc1gf4iMx4h5aCHT
github.com/grafana/grafana-google-sdk-go v0.2.1/go.mod h1:RiITSHwBhqVTTd3se3HQq5Ncs/wzzhTB9OK5N0J0PEU=
github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 h1:r+mU5bGMzcXCRVAuOrTn54S80qbfVkvTdUJZfSfTNbs=
github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79/go.mod h1:wc6Hbh3K2TgCUSfBC/BOzabItujtHMESZeFk5ZhdxhQ=
github.com/grafana/grafana-plugin-sdk-go v0.266.0 h1:YP+iEpXH3HRX9Xo4NHjsrJhN2W7uVTtkLNzMHYbmiLI=
github.com/grafana/grafana-plugin-sdk-go v0.266.0/go.mod h1:bxkXrBQ4QSmOncsWdIOcpgP+M6wajQNMAPXlbWrqAWY=
github.com/grafana/grafana-plugin-sdk-go v0.267.0 h1:4AcuLEE0UeFG0Eo3P8r1FDS89hgZZ73K550DRz1fgjQ=
github.com/grafana/grafana-plugin-sdk-go v0.267.0/go.mod h1:OuwS4c/JYgn0rr/w5zhJBpLo4gKm/vw15RsfpYAvK9Q=
github.com/grafana/grafana/apps/advisor v0.0.0-20250220163425-b4c4b9abbdc8 h1:mG/6nDlEBVxWlo2GQJVASzucw3ByPIBsec06XcPrjgQ=
github.com/grafana/grafana/apps/advisor v0.0.0-20250220163425-b4c4b9abbdc8/go.mod h1:9I1dKV3Dqr0NPR9Af0WJGxOytp5/6W3JLiNChOz8r+c=
github.com/grafana/grafana/apps/alerting/notifications v0.0.0-20250220163425-b4c4b9abbdc8 h1:w42GlvkmHG4nM/p1kb2nKmROVP+AHtL3qWEYMhnhCVM=
@@ -3357,8 +3358,8 @@ google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
+4 -16
View File
@@ -142,8 +142,6 @@ cloud.google.com/go/memcache v1.11.1 h1:2FGuyd3WY7buNDAkMBdmeIOheNWA3gwaXrttLrEd
cloud.google.com/go/memcache v1.11.1/go.mod h1:3zF+dEqmEmElHuO4NtHiShekQY5okQtssjPBv7jpmZ8=
cloud.google.com/go/metastore v1.14.1 h1:kGx+IUSSYCVn8LisCT4fpxCC9rauEVonzi7RlygdqWY=
cloud.google.com/go/metastore v1.14.1/go.mod h1:WDvsAcbQLl9M4xL+eIpbKogH7aEaPWMhO9aRBcFOnJE=
cloud.google.com/go/monitoring v1.21.1 h1:zWtbIoBMnU5LP9A/fz8LmWMGHpk4skdfeiaa66QdFGc=
cloud.google.com/go/monitoring v1.21.1/go.mod h1:Rj++LKrlht9uBi8+Eb530dIrzG/cU/lB8mt+lbeFK1c=
cloud.google.com/go/networkconnectivity v1.15.1 h1:EizN+cFGHzRAyiFTK8jT1PqTo+cSnbc2IGh6OmllS7Y=
cloud.google.com/go/networkconnectivity v1.15.1/go.mod h1:tYAcT4Ahvq+BiePXL/slYipf/8FF0oNJw3MqFhBnSPI=
cloud.google.com/go/networkmanagement v1.14.1 h1:0x3hVI6xbp3N/choffKPHMSxbzaPdHSD92cBElebXEk=
@@ -204,8 +202,6 @@ cloud.google.com/go/servicemanagement v1.8.0 h1:fopAQI/IAzlxnVeiKn/8WiV6zKndjFkv
cloud.google.com/go/serviceusage v1.6.0 h1:rXyq+0+RSIm3HFypctp7WoXxIA563rn206CfMWdqXX4=
cloud.google.com/go/shell v1.8.1 h1:etoJal+LB7Pn8+5vE2aAh6QcFbBmerIOh5MxNDoXykw=
cloud.google.com/go/shell v1.8.1/go.mod h1:jaU7OHeldDhTwgs3+clM0KYEDYnBAPevUI6wNLf7ycE=
cloud.google.com/go/spanner v1.70.0 h1:nj6p/GJTgMDiSQ1gQ034ItsKuJgHiMOjtOlONOg8PSo=
cloud.google.com/go/spanner v1.70.0/go.mod h1:X5T0XftydYp0K1adeJQDJtdWpbrOeJ7wHecM4tK6FiE=
cloud.google.com/go/speech v1.25.1 h1:iGZJS3wrdkje/Vqiacx1+r+zVwUZoXVMdklYIVsvfNw=
cloud.google.com/go/speech v1.25.1/go.mod h1:WgQghvghkZ1htG6BhYn98mP7Tg0mti8dBFDLMVXH/vM=
cloud.google.com/go/storagetransfer v1.11.1 h1:Hd7H1zXGQGEWyWXxWVXDMuNCGasNQim1y9CIaMZIBX8=
@@ -280,10 +276,6 @@ github.com/DmitriyVTitov/size v1.5.0 h1:/PzqxYrOyOUX1BXj6J9OuVRVGe+66VL4D9FlUaW5
github.com/DmitriyVTitov/size v1.5.0/go.mod h1:le6rNI4CoLQV1b9gzp1+3d7hMAD/uu2QcJ+aYbNgiU0=
github.com/GoogleCloudPlatform/cloudsql-proxy v1.36.0 h1:kAtNAWwvTt5+iew6baV0kbOrtjYTXPtWNSyOFlcxkBU=
github.com/GoogleCloudPlatform/cloudsql-proxy v1.36.0/go.mod h1:VRKXU8C7Y/aUKjRBTGfw0Ndv4YqNxlB8zAPJJDxbASE=
github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.0 h1:oVLqHXhnYtUwM89y9T1fXGaK9wTkXHgNp8/ZNMQzUxE=
github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.0/go.mod h1:dppbR7CwXD4pgtV9t3wD1812RaLDcBjtblcDF5f1vI0=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 h1:3c8yed4lgqTt+oTQ+JNMDo+F4xprBf+O/il4ZC0nRLw=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0/go.mod h1:obipzmGjfSjam60XLwGfqUkJsfiheAl+TUjG+4yzyPM=
github.com/IBM/go-sdk-core/v5 v5.17.4 h1:VGb9+mRrnS2HpHZFM5hy4J6ppIWnwNrw0G+tLSgcJLc=
github.com/IBM/go-sdk-core/v5 v5.17.4/go.mod h1:KsAAI7eStAWwQa4F96MLy+whYSh39JzNjklZRbN/8ns=
github.com/IBM/ibm-cos-sdk-go v1.11.0 h1:Jp55NLN3OvBwucMGpP5wNybyjncsmTZ9+GPHai/1cE8=
@@ -387,7 +379,6 @@ github.com/bytedance/sonic v1.10.0-rc3 h1:uNSnscRapXTwUgTyOF0GVljYD08p9X/Lbr9Mwe
github.com/bytedance/sonic v1.10.0-rc3/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
github.com/campoy/embedmd v1.0.0 h1:V4kI2qTJJLf4J29RzI/MAt2c3Bl4dQSYPuflzwFH2hY=
github.com/campoy/embedmd v1.0.0/go.mod h1:oxyr9RCiSXg0M3VJ3ks0UGfp98BpSSGr0kpiX3MzVl8=
github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo=
@@ -510,6 +501,7 @@ github.com/elastic/go-sysinfo v1.11.2/go.mod h1:GKqR8bbMK/1ITnez9NIsIfXQr25aLhRJ
github.com/elastic/go-windows v1.0.1 h1:AlYZOldA+UJ0/2nBuqWdo90GFCgG9xuyw9SYzGUtJm0=
github.com/elastic/go-windows v1.0.1/go.mod h1:FoVvqWSun28vaDQPbj2Elfc0JahhPB7WQEGa3c814Ss=
github.com/elazarl/goproxy v1.3.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ=
github.com/elazarl/goproxy v1.7.1 h1:1P7LPSxbqtNxusFnXclj6O56pjfq1xOQZ6a0mwwKUlY=
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633 h1:H2pdYOb3KQ1/YsqVWoWNLQO+fusocsw354rqGTZtAgw=
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
@@ -593,6 +585,7 @@ github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs0
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/cel-go v0.22.0/go.mod h1:BuznPXXfQDpXKWQ9sPW3TzlAJN5zzFe+i9tIs0yC4s8=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY=
github.com/google/go-jsonnet v0.18.0 h1:/6pTy6g+Jh1a1I2UMoAODkqELFiVIdOxbNwv0DDzoOg=
github.com/google/go-jsonnet v0.18.0/go.mod h1:C3fTzyVJDslXdiTqw/bTFk7vSGyCtH3MGRbDfvEwGd0=
@@ -907,7 +900,6 @@ github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad h1:fiWzISvDn0Csy5H0iwgAuJ
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
github.com/stoewer/parquet-cli v0.0.7 h1:rhdZODIbyMS3twr4OM3am8BPPT5pbfMcHLH93whDM5o=
github.com/stoewer/parquet-cli v0.0.7/go.mod h1:bskxHdj8q3H1EmfuCqjViFoeO3NEvs5lzZAQvI8Nfjk=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/substrait-io/substrait v0.57.1 h1:GW8nnYfSowMseHR8Os82/X6lNtQGIK7p4p+lr6r+auw=
github.com/substrait-io/substrait v0.57.1/go.mod h1:q9s+tjo+gK0lsA+SqYB0lhojNuxvdPdfYlGUP0hjbrA=
github.com/substrait-io/substrait-go v1.2.0 h1:3ZNRkc8FYD7ifCagKEOZQtUcgMceMQfwo2N1NGaK4Q4=
@@ -1055,8 +1047,6 @@ go.opentelemetry.io/contrib/bridges/prometheus v0.53.0 h1:BdkKDtcrHThgjcEia1737O
go.opentelemetry.io/contrib/bridges/prometheus v0.53.0/go.mod h1:ZkhVxcJgeXlL/lVyT/vxNHVFiSG5qOaDwYaSgD8IfZo=
go.opentelemetry.io/contrib/config v0.7.0 h1:b1rK5tGTuhhPirJiMxOcyQfZs76j2VapY6ODn3b2Dbs=
go.opentelemetry.io/contrib/config v0.7.0/go.mod h1:8tdiFd8N5etOi3XzBmAoMxplEzI3TcL8dU5rM5/xcOQ=
go.opentelemetry.io/contrib/detectors/gcp v1.32.0 h1:P78qWqkLSShicHmAzfECaTgvslqHxblNE9j62Ws1NK8=
go.opentelemetry.io/contrib/detectors/gcp v1.32.0/go.mod h1:TVqo0Sda4Cv8gCIixd7LuLwW4EylumVWfhjZJjDD4DU=
go.opentelemetry.io/contrib/exporters/autoexport v0.53.0 h1:13K+tY7E8GJInkrvRiPAhC0gi/7vKjzDNhtmCf+QXG8=
go.opentelemetry.io/contrib/exporters/autoexport v0.53.0/go.mod h1:lyQF6xQ4iDnMg4sccNdFs1zf62xd79YI8vZqKjOTwMs=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74=
@@ -1137,12 +1127,9 @@ golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -1200,6 +1187,7 @@ google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojt
google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
+3 -3
View File
@@ -31,7 +31,7 @@
"lint": "yarn run lint:ts && yarn run lint:sass",
"lint:ts": "eslint ./ ./public/app/extensions/ --cache --no-error-on-unmatched-pattern",
"lint:sass": "yarn stylelint '{public/sass,packages}/**/*.scss' --cache",
"test:ci": "mkdir -p reports/junit && JEST_JUNIT_OUTPUT_DIR=reports/junit jest --ci --reporters=default --reporters=jest-junit -w ${TEST_MAX_WORKERS:-100%}",
"test:ci": "mkdir -p reports/junit && JEST_JUNIT_OUTPUT_DIR=reports/junit jest --ci --reporters=default --reporters=jest-junit -w ${TEST_MAX_WORKERS:-100%} --shard=${TEST_SHARD:-1}/${TEST_SHARD_TOTAL:-1}",
"lint:fix": "yarn lint:ts --fix",
"packages:build": "nx run-many -t build --projects='tag:scope:package'",
"packages:clean": "rimraf ./npm-artifacts && nx run-many -t clean --projects='tag:scope:package' --maxParallel=100",
@@ -276,8 +276,8 @@
"@grafana/prometheus": "workspace:*",
"@grafana/runtime": "workspace:*",
"@grafana/saga-icons": "workspace:*",
"@grafana/scenes": "6.0.2",
"@grafana/scenes-react": "6.0.2",
"@grafana/scenes": "6.1.4",
"@grafana/scenes-react": "6.1.4",
"@grafana/schema": "workspace:*",
"@grafana/sql": "workspace:*",
"@grafana/ui": "workspace:*",
+2
View File
@@ -576,6 +576,8 @@ export {
type ScopeNodeSpec,
type ScopeNode,
scopeFilterOperatorMap,
reverseScopeFilterOperatorMap,
isEqualityOrMultiOperator,
} from './types/scopes';
export {
PluginState,
+11
View File
@@ -18,6 +18,12 @@ export interface ScopeDashboardBinding {
}
export type ScopeFilterOperator = 'equals' | 'not-equals' | 'regex-match' | 'regex-not-match' | 'one-of' | 'not-one-of';
export type EqualityOrMultiOperator = Extract<ScopeFilterOperator, 'equals' | 'not-equals' | 'one-of' | 'not-one-of'>;
export function isEqualityOrMultiOperator(value: string): value is EqualityOrMultiOperator {
const operators = new Set(['equals', 'not-equals', 'one-of', 'not-one-of']);
return operators.has(value);
}
export const scopeFilterOperatorMap: Record<string, ScopeFilterOperator> = {
'=': 'equals',
@@ -28,6 +34,11 @@ export const scopeFilterOperatorMap: Record<string, ScopeFilterOperator> = {
'!=|': 'not-one-of',
};
export const reverseScopeFilterOperatorMap: Record<ScopeFilterOperator, string> = Object.fromEntries(
Object.entries(scopeFilterOperatorMap).map(([symbol, operator]) => [operator, symbol])
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
) as Record<ScopeFilterOperator, string>;
export interface ScopeSpecFilter {
key: string;
value: string;
@@ -574,6 +574,7 @@ ResponsiveGridLayoutItemKind: {
ResponsiveGridLayoutItemSpec: {
element: ElementReference
repeat?: ResponsiveGridRepeatOptions
}
TabsLayoutKind: {
@@ -867,6 +867,7 @@ export const defaultResponsiveGridLayoutItemKind = (): ResponsiveGridLayoutItemK
export interface ResponsiveGridLayoutItemSpec {
element: ElementReference;
repeat?: ResponsiveGridRepeatOptions;
}
export const defaultResponsiveGridLayoutItemSpec = (): ResponsiveGridLayoutItemSpec => ({
+4 -4
View File
@@ -6,7 +6,7 @@ toolchain go1.23.6
require (
github.com/emicklei/go-restful/v3 v3.11.0
github.com/grafana/grafana-plugin-sdk-go v0.266.0
github.com/grafana/grafana-plugin-sdk-go v0.267.0
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240808213237-f4d2e064f435
github.com/grafana/grafana/pkg/semconv v0.0.0-20240808213237-f4d2e064f435
github.com/mattbaird/jsonpatch v0.0.0-20240118010651-0ba75a80ca38
@@ -39,7 +39,7 @@ require (
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/elazarl/goproxy v1.7.0 // indirect
github.com/elazarl/goproxy v1.7.1 // indirect
github.com/evanphx/json-patch v5.6.0+incompatible // indirect
github.com/fatih/color v1.17.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
@@ -59,7 +59,7 @@ require (
github.com/google/cel-go v0.22.1 // indirect
github.com/google/flatbuffers v24.3.25+incompatible // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect
github.com/google/uuid v1.6.0 // indirect
@@ -150,7 +150,7 @@ require (
google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
google.golang.org/grpc v1.70.0 // indirect
google.golang.org/protobuf v1.36.4 // indirect
google.golang.org/protobuf v1.36.5 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
+8 -7
View File
@@ -49,8 +49,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/elazarl/goproxy v1.7.0 h1:EXv2nV4EjM60ZtsEVLYJG4oBXhDGutMKperpHsZ/v+0=
github.com/elazarl/goproxy v1.7.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ=
github.com/elazarl/goproxy v1.7.1 h1:1P7LPSxbqtNxusFnXclj6O56pjfq1xOQZ6a0mwwKUlY=
github.com/elazarl/goproxy v1.7.1/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@@ -118,8 +118,9 @@ github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvR
github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -134,8 +135,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grafana/grafana-plugin-sdk-go v0.266.0 h1:YP+iEpXH3HRX9Xo4NHjsrJhN2W7uVTtkLNzMHYbmiLI=
github.com/grafana/grafana-plugin-sdk-go v0.266.0/go.mod h1:bxkXrBQ4QSmOncsWdIOcpgP+M6wajQNMAPXlbWrqAWY=
github.com/grafana/grafana-plugin-sdk-go v0.267.0 h1:4AcuLEE0UeFG0Eo3P8r1FDS89hgZZ73K550DRz1fgjQ=
github.com/grafana/grafana-plugin-sdk-go v0.267.0/go.mod h1:OuwS4c/JYgn0rr/w5zhJBpLo4gKm/vw15RsfpYAvK9Q=
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240808213237-f4d2e064f435 h1:lmw60EW7JWlAEvgggktOyVkH4hF1m/+LSF/Ap0NCyi8=
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240808213237-f4d2e064f435/go.mod h1:ORVFiW/KNRY52lNjkGwnFWCxNVfE97bJG2jr2fetq0I=
github.com/grafana/grafana/pkg/semconv v0.0.0-20240808213237-f4d2e064f435 h1:SNEeqY22DrGr5E9kGF1mKSqlOom14W9+b1u4XEGJowA=
@@ -495,8 +496,8 @@ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+2 -1
View File
@@ -23,6 +23,7 @@ require (
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
@@ -43,7 +44,7 @@ require (
golang.org/x/text v0.22.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
google.golang.org/grpc v1.70.0 // indirect
google.golang.org/protobuf v1.36.4 // indirect
google.golang.org/protobuf v1.36.5 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
+4 -4
View File
@@ -25,8 +25,8 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6
github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -151,8 +151,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+2 -2
View File
@@ -5,7 +5,7 @@ go 1.23.4
toolchain go1.23.6
require (
github.com/google/go-cmp v0.6.0
github.com/google/go-cmp v0.7.0
github.com/grafana/authlib/types v0.0.0-20250224151205-5ef97131cc82
github.com/grafana/grafana-app-sdk/logging v0.30.0
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240701135906-559738ce6ae1
@@ -93,7 +93,7 @@ require (
google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
google.golang.org/grpc v1.70.0 // indirect
google.golang.org/protobuf v1.36.4 // indirect
google.golang.org/protobuf v1.36.5 // 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
+4 -4
View File
@@ -68,8 +68,8 @@ github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvR
github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -312,8 +312,8 @@ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+18 -23
View File
@@ -26,8 +26,18 @@ var (
_ rest.SingularNameProvider = (DualWriter)(nil)
)
type dualWriteContextKey struct{}
func IsDualWriteUpdate(ctx context.Context) bool {
return ctx.Value(dualWriteContextKey{}) == true
}
func WithDualWriteUpdate(ctx context.Context) context.Context {
return context.WithValue(ctx, dualWriteContextKey{}, true)
}
// Function that will create a dual writer
type DualWriteBuilder func(gr schema.GroupResource, legacy LegacyStorage, storage Storage) (Storage, error)
type DualWriteBuilder func(gr schema.GroupResource, legacy Storage, unified Storage) (Storage, error)
// Storage is a storage implementation that satisfies the same interfaces as genericregistry.Store.
type Storage interface {
@@ -36,26 +46,12 @@ type Storage interface {
rest.TableConvertor
rest.SingularNameProvider
rest.Getter
// TODO: when watch is implemented, we can replace all the below with rest.StandardStorage
rest.Lister
rest.CreaterUpdater
rest.GracefulDeleter
rest.CollectionDeleter
}
// LegacyStorage is a storage implementation that writes to the Grafana SQL database.
type LegacyStorage interface {
rest.Storage
rest.Scoper
rest.SingularNameProvider
rest.CreaterUpdater
rest.Lister
rest.GracefulDeleter
rest.CollectionDeleter
rest.TableConvertor
rest.Getter
}
// DualWriter is a storage implementation that writes first to LegacyStorage and then to Storage.
// If writing to LegacyStorage fails, the write to Storage is skipped and the error is returned.
// Storage is used for all read operations. This is useful as a migration step from SQL based
@@ -79,7 +75,6 @@ type LegacyStorage interface {
type DualWriter interface {
Storage
LegacyStorage
Mode() DualWriterMode
}
@@ -110,8 +105,8 @@ const (
// NewDualWriter returns a new DualWriter.
func NewDualWriter(
mode DualWriterMode,
legacy LegacyStorage,
storage Storage,
legacy Storage,
unified Storage,
reg prometheus.Registerer,
resource string,
) Storage {
@@ -122,17 +117,17 @@ func NewDualWriter(
return legacy
case Mode1:
// read and write only from legacy storage
return newDualWriterMode1(legacy, storage, metrics, resource)
return newDualWriterMode1(legacy, unified, metrics, resource)
case Mode2:
// write to both, read from storage but use legacy as backup
return newDualWriterMode2(legacy, storage, metrics, resource)
return newDualWriterMode2(legacy, unified, metrics, resource)
case Mode3:
// write to both, read from storage only
return newDualWriterMode3(legacy, storage, metrics, resource)
return newDualWriterMode3(legacy, unified, metrics, resource)
case Mode4, Mode5:
return storage
return unified
default:
return newDualWriterMode1(legacy, storage, metrics, resource)
return newDualWriterMode1(legacy, unified, metrics, resource)
}
}
+2 -2
View File
@@ -15,7 +15,7 @@ import (
)
type DualWriterMode1 struct {
Legacy LegacyStorage
Legacy Storage
Storage Storage
*dualWriterMetrics
resource string
@@ -26,7 +26,7 @@ const mode1Str = "1"
// NewDualWriterMode1 returns a new DualWriter in mode 1.
// Mode 1 represents writing to and reading from LegacyStorage.
func newDualWriterMode1(legacy LegacyStorage, storage Storage, dwm *dualWriterMetrics, resource string) *DualWriterMode1 {
func newDualWriterMode1(legacy Storage, storage Storage, dwm *dualWriterMetrics, resource string) *DualWriterMode1 {
return &DualWriterMode1{
Legacy: legacy,
Storage: storage,
+24 -24
View File
@@ -60,10 +60,10 @@ func TestMode1_Create(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := (LegacyStorage)(nil)
l := (Storage)(nil)
s := (Storage)(nil)
ls := legacyStoreMock{&mock.Mock{}, l}
ls := storageMock{&mock.Mock{}, l}
us := storageMock{&mock.Mock{}, s}
if tt.setupLegacyFn != nil {
@@ -122,10 +122,10 @@ func TestMode1_CreateOnUnifiedStorage(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := (LegacyStorage)(nil)
l := (Storage)(nil)
s := (Storage)(nil)
ls := legacyStoreMock{&mock.Mock{}, l}
ls := storageMock{&mock.Mock{}, l}
us := storageMock{&mock.Mock{}, s}
if tt.setupLegacyFn != nil {
@@ -190,10 +190,10 @@ func TestMode1_Get(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := (LegacyStorage)(nil)
l := (Storage)(nil)
s := (Storage)(nil)
ls := legacyStoreMock{&mock.Mock{}, l}
ls := storageMock{&mock.Mock{}, l}
us := storageMock{&mock.Mock{}, s}
if tt.setupLegacyFn != nil {
@@ -251,10 +251,10 @@ func TestMode1_GetFromUnifiedStorage(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := (LegacyStorage)(nil)
l := (Storage)(nil)
s := (Storage)(nil)
ls := legacyStoreMock{&mock.Mock{}, l}
ls := storageMock{&mock.Mock{}, l}
us := storageMock{&mock.Mock{}, s}
if tt.setupLegacyFn != nil {
@@ -308,10 +308,10 @@ func TestMode1_List(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := (LegacyStorage)(nil)
l := (Storage)(nil)
s := (Storage)(nil)
ls := legacyStoreMock{&mock.Mock{}, l}
ls := storageMock{&mock.Mock{}, l}
us := storageMock{&mock.Mock{}, s}
if tt.setupLegacyFn != nil {
@@ -365,10 +365,10 @@ func TestMode1_ListFromUnifiedStorage(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := (LegacyStorage)(nil)
l := (Storage)(nil)
s := (Storage)(nil)
ls := legacyStoreMock{&mock.Mock{}, l}
ls := storageMock{&mock.Mock{}, l}
us := storageMock{&mock.Mock{}, s}
if tt.setupLegacyFn != nil {
@@ -434,10 +434,10 @@ func TestMode1_Delete(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := (LegacyStorage)(nil)
l := (Storage)(nil)
s := (Storage)(nil)
ls := legacyStoreMock{&mock.Mock{}, l}
ls := storageMock{&mock.Mock{}, l}
us := storageMock{&mock.Mock{}, s}
if tt.setupLegacyFn != nil {
@@ -494,10 +494,10 @@ func TestMode1_DeleteFromUnifiedStorage(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := (LegacyStorage)(nil)
l := (Storage)(nil)
s := (Storage)(nil)
ls := legacyStoreMock{&mock.Mock{}, l}
ls := storageMock{&mock.Mock{}, l}
us := storageMock{&mock.Mock{}, s}
if tt.setupLegacyFn != nil {
@@ -565,10 +565,10 @@ func TestMode1_DeleteCollection(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := (LegacyStorage)(nil)
l := (Storage)(nil)
s := (Storage)(nil)
ls := legacyStoreMock{&mock.Mock{}, l}
ls := storageMock{&mock.Mock{}, l}
us := storageMock{&mock.Mock{}, s}
if tt.setupLegacyFn != nil {
@@ -626,10 +626,10 @@ func TestMode1_DeleteCollectionFromUnifiedStorage(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := (LegacyStorage)(nil)
l := (Storage)(nil)
s := (Storage)(nil)
ls := legacyStoreMock{&mock.Mock{}, l}
ls := storageMock{&mock.Mock{}, l}
us := storageMock{&mock.Mock{}, s}
if tt.setupLegacyFn != nil {
@@ -695,10 +695,10 @@ func TestMode1_Update(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := (LegacyStorage)(nil)
l := (Storage)(nil)
s := (Storage)(nil)
ls := legacyStoreMock{&mock.Mock{}, l}
ls := storageMock{&mock.Mock{}, l}
us := storageMock{&mock.Mock{}, s}
if tt.setupLegacyFn != nil {
@@ -761,10 +761,10 @@ func TestMode1_UpdateOnUnifiedStorage(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := (LegacyStorage)(nil)
l := (Storage)(nil)
s := (Storage)(nil)
ls := legacyStoreMock{&mock.Mock{}, l}
ls := storageMock{&mock.Mock{}, l}
us := storageMock{&mock.Mock{}, s}
if tt.setupLegacyFn != nil {
+2 -8
View File
@@ -16,15 +16,9 @@ import (
"github.com/grafana/grafana/pkg/apimachinery/utils"
)
type dualWriteContextKey struct{}
func IsDualWriteUpdate(ctx context.Context) bool {
return ctx.Value(dualWriteContextKey{}) == true
}
type DualWriterMode2 struct {
Storage Storage
Legacy LegacyStorage
Legacy Storage
*dualWriterMetrics
resource string
Log klog.Logger
@@ -35,7 +29,7 @@ const mode2Str = "2"
// newDualWriterMode2 returns a new DualWriter in mode 2.
// Mode 2 represents writing to LegacyStorage first, then to Storage.
// When reading, values from LegacyStorage will be returned.
func newDualWriterMode2(legacy LegacyStorage, storage Storage, dwm *dualWriterMetrics, resource string) *DualWriterMode2 {
func newDualWriterMode2(legacy Storage, storage Storage, dwm *dualWriterMetrics, resource string) *DualWriterMode2 {
return &DualWriterMode2{
Legacy: legacy,
Storage: storage,
+12 -12
View File
@@ -54,10 +54,10 @@ func TestMode2_Create(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := (LegacyStorage)(nil)
l := (Storage)(nil)
s := (Storage)(nil)
ls := legacyStoreMock{&mock.Mock{}, l}
ls := storageMock{&mock.Mock{}, l}
us := storageMock{&mock.Mock{}, s}
if tt.setupLegacyFn != nil {
@@ -138,10 +138,10 @@ func TestMode2_Get(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := (LegacyStorage)(nil)
l := (Storage)(nil)
s := (Storage)(nil)
ls := legacyStoreMock{&mock.Mock{}, l}
ls := storageMock{&mock.Mock{}, l}
us := storageMock{&mock.Mock{}, s}
if tt.setupLegacyFn != nil {
@@ -212,10 +212,10 @@ func TestMode2_List(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := (LegacyStorage)(nil)
l := (Storage)(nil)
s := (Storage)(nil)
ls := legacyStoreMock{&mock.Mock{}, l}
ls := storageMock{&mock.Mock{}, l}
us := storageMock{&mock.Mock{}, s}
if tt.setupLegacyFn != nil {
@@ -313,10 +313,10 @@ func TestMode2_Delete(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := (LegacyStorage)(nil)
l := (Storage)(nil)
s := (Storage)(nil)
ls := legacyStoreMock{&mock.Mock{}, l}
ls := storageMock{&mock.Mock{}, l}
us := storageMock{&mock.Mock{}, s}
if tt.setupLegacyFn != nil {
@@ -382,10 +382,10 @@ func TestMode2_DeleteCollection(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := (LegacyStorage)(nil)
l := (Storage)(nil)
s := (Storage)(nil)
ls := legacyStoreMock{&mock.Mock{}, l}
ls := storageMock{&mock.Mock{}, l}
us := storageMock{&mock.Mock{}, s}
if tt.setupLegacyFn != nil {
@@ -451,10 +451,10 @@ func TestMode2_Update(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := (LegacyStorage)(nil)
l := (Storage)(nil)
s := (Storage)(nil)
ls := legacyStoreMock{&mock.Mock{}, l}
ls := storageMock{&mock.Mock{}, l}
us := storageMock{&mock.Mock{}, s}
if tt.setupLegacyFn != nil {
+2 -2
View File
@@ -17,7 +17,7 @@ import (
)
type DualWriterMode3 struct {
Legacy LegacyStorage
Legacy Storage
Storage Storage
watchImp rest.Watcher // watch is only available in mode 3 and 4
*dualWriterMetrics
@@ -27,7 +27,7 @@ type DualWriterMode3 struct {
// newDualWriterMode3 returns a new DualWriter in mode 3.
// Mode 3 represents writing to LegacyStorage and Storage and reading from Storage.
func newDualWriterMode3(legacy LegacyStorage, storage Storage, dwm *dualWriterMetrics, resource string) *DualWriterMode3 {
func newDualWriterMode3(legacy Storage, storage Storage, dwm *dualWriterMetrics, resource string) *DualWriterMode3 {
return &DualWriterMode3{
Legacy: legacy,
Storage: storage,
+14 -14
View File
@@ -61,10 +61,10 @@ func TestMode3_Create(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := (LegacyStorage)(nil)
l := (Storage)(nil)
s := (Storage)(nil)
ls := legacyStoreMock{&mock.Mock{}, l}
ls := storageMock{&mock.Mock{}, l}
us := storageMock{&mock.Mock{}, s}
if tt.setupLegacyFn != nil {
@@ -128,10 +128,10 @@ func TestMode3_Get(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := (LegacyStorage)(nil)
l := (Storage)(nil)
s := (Storage)(nil)
ls := legacyStoreMock{&mock.Mock{}, l}
ls := storageMock{&mock.Mock{}, l}
us := storageMock{&mock.Mock{}, s}
if tt.setupLegacyFn != nil {
@@ -187,10 +187,10 @@ func TestMode1_GetFromLegacyStorage(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := (LegacyStorage)(nil)
l := (Storage)(nil)
s := (Storage)(nil)
ls := legacyStoreMock{&mock.Mock{}, l}
ls := storageMock{&mock.Mock{}, l}
us := storageMock{&mock.Mock{}, s}
if tt.setupLegacyFn != nil {
@@ -234,10 +234,10 @@ func TestMode3_List(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := (LegacyStorage)(nil)
l := (Storage)(nil)
s := (Storage)(nil)
ls := legacyStoreMock{&mock.Mock{}, l}
ls := storageMock{&mock.Mock{}, l}
us := storageMock{&mock.Mock{}, s}
if tt.setupStorageFn != nil {
@@ -311,10 +311,10 @@ func TestMode3_Delete(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := (LegacyStorage)(nil)
l := (Storage)(nil)
s := (Storage)(nil)
ls := legacyStoreMock{&mock.Mock{}, l}
ls := storageMock{&mock.Mock{}, l}
us := storageMock{&mock.Mock{}, s}
if tt.setupLegacyFn != nil {
@@ -380,10 +380,10 @@ func TestMode3_DeleteCollection(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := (LegacyStorage)(nil)
l := (Storage)(nil)
s := (Storage)(nil)
ls := legacyStoreMock{&mock.Mock{}, l}
ls := storageMock{&mock.Mock{}, l}
us := storageMock{&mock.Mock{}, s}
if tt.setupLegacyFn != nil {
@@ -449,10 +449,10 @@ func TestMode3_Update(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := (LegacyStorage)(nil)
l := (Storage)(nil)
s := (Storage)(nil)
ls := legacyStoreMock{&mock.Mock{}, l}
ls := storageMock{&mock.Mock{}, l}
us := storageMock{&mock.Mock{}, s}
if tt.setupLegacyFn != nil {
+1 -1
View File
@@ -33,7 +33,7 @@ type SyncerConfig struct {
RequestInfo *request.RequestInfo
Mode DualWriterMode
LegacyStorage LegacyStorage
LegacyStorage Storage
Storage Storage
ServerLockService ServerLockService
+4 -4
View File
@@ -181,12 +181,12 @@ func TestLegacyToUnifiedStorage_DataSyncer(t *testing.T) {
// mode 1
for _, tt := range tests {
t.Run("Mode-1-"+tt.name, func(t *testing.T) {
l := (LegacyStorage)(nil)
l := (Storage)(nil)
s := (Storage)(nil)
lm := &mock.Mock{}
um := &mock.Mock{}
ls := legacyStoreMock{lm, l}
ls := storageMock{lm, l}
us := storageMock{um, s}
if tt.setupLegacyFn != nil {
@@ -221,12 +221,12 @@ func TestLegacyToUnifiedStorage_DataSyncer(t *testing.T) {
// mode 2
for _, tt := range tests {
t.Run("Mode-2-"+tt.name, func(t *testing.T) {
l := (LegacyStorage)(nil)
l := (Storage)(nil)
s := (Storage)(nil)
lm := &mock.Mock{}
um := &mock.Mock{}
ls := legacyStoreMock{lm, l}
ls := storageMock{lm, l}
us := storageMock{um, s}
if tt.setupLegacyFn != nil {
+2 -2
View File
@@ -64,7 +64,7 @@ func TestSetDualWritingMode(t *testing.T) {
}
for _, tt := range tests {
l := (LegacyStorage)(nil)
l := (Storage)(nil)
s := (Storage)(nil)
sm := &mock.Mock{}
@@ -75,7 +75,7 @@ func TestSetDualWritingMode(t *testing.T) {
lm := &mock.Mock{}
lm.On("List", mock.Anything, mock.Anything).Return(exampleList, nil)
ls := legacyStoreMock{lm, l}
ls := storageMock{lm, l}
serverLockSvc := &fakeServerLock{
err: tt.serverLockError,
-91
View File
@@ -11,102 +11,11 @@ import (
"k8s.io/apiserver/pkg/registry/rest"
)
type legacyStoreMock struct {
*mock.Mock
LegacyStorage
}
type storageMock struct {
*mock.Mock
Storage
}
func (m legacyStoreMock) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
select {
case <-ctx.Done():
return nil, errors.New("context canceled")
default:
}
args := m.Called(ctx, name, options)
if err := args.Get(1); err != nil {
return nil, err.(error)
}
return args.Get(0).(runtime.Object), args.Error(1)
}
func (m legacyStoreMock) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
select {
case <-ctx.Done():
return nil, errors.New("context canceled")
default:
}
args := m.Called(ctx, obj, createValidation, options)
if err := args.Get(1); err != nil {
return nil, err.(error)
}
return args.Get(0).(runtime.Object), args.Error(1)
}
func (m legacyStoreMock) List(ctx context.Context, options *metainternalversion.ListOptions) (runtime.Object, error) {
select {
case <-ctx.Done():
return nil, errors.New("context canceled")
default:
}
args := m.Called(ctx, options)
if err := args.Get(1); err != nil {
return nil, err.(error)
}
return args.Get(0).(runtime.Object), args.Error(1)
}
func (m legacyStoreMock) NewList() runtime.Object {
return nil
}
func (m legacyStoreMock) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) {
select {
case <-ctx.Done():
return nil, false, errors.New("context canceled")
default:
}
args := m.Called(ctx, name, objInfo, createValidation, updateValidation, forceAllowCreate, options)
if err := args.Get(2); err != nil {
return nil, false, err.(error)
}
return args.Get(0).(runtime.Object), args.Bool(1), args.Error(2)
}
func (m legacyStoreMock) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
select {
case <-ctx.Done():
return nil, false, errors.New("context canceled")
default:
}
args := m.Called(ctx, name, deleteValidation, options)
if err := args.Get(2); err != nil {
return nil, false, err.(error)
}
return args.Get(0).(runtime.Object), args.Bool(1), args.Error(2)
}
func (m legacyStoreMock) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *metainternalversion.ListOptions) (runtime.Object, error) {
select {
case <-ctx.Done():
return nil, errors.New("context canceled")
default:
}
args := m.Called(ctx, deleteValidation, options, listOptions)
if err := args.Get(1); err != nil {
return nil, err.(error)
}
return args.Get(0).(runtime.Object), args.Error(1)
}
// Unified Store
func (m storageMock) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
select {
+2 -2
View File
@@ -14,7 +14,7 @@ require (
github.com/docker/docker v27.4.1+incompatible // @grafana/grafana-developer-enablement-squad
github.com/drone/drone-cli v1.8.0 // @grafana/grafana-developer-enablement-squad
github.com/gogo/protobuf v1.3.2 // indirect; @grafana/alerting-backend
github.com/google/go-cmp v0.6.0 // @grafana/grafana-backend-group
github.com/google/go-cmp v0.7.0 // @grafana/grafana-backend-group
github.com/google/go-github/v69 v69.2.0 // @grafana/grafana-developer-enablement-squad
github.com/google/uuid v1.6.0 // indirect; @grafana/grafana-backend-group
github.com/googleapis/gax-go/v2 v2.14.1 // indirect; @grafana/grafana-backend-group
@@ -35,7 +35,7 @@ require (
golang.org/x/time v0.9.0 // indirect; @grafana/grafana-backend-group
google.golang.org/api v0.216.0 // @grafana/grafana-backend-group
google.golang.org/grpc v1.70.0 // indirect; @grafana/plugins-platform-backend
google.golang.org/protobuf v1.36.4 // indirect; @grafana/plugins-platform-backend
google.golang.org/protobuf v1.36.5 // indirect; @grafana/plugins-platform-backend
gopkg.in/yaml.v3 v3.0.1 // @grafana/alerting-backend
)
+4 -4
View File
@@ -126,8 +126,8 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-github/v69 v69.2.0 h1:wR+Wi/fN2zdUx9YxSmYE0ktiX9IAR/BeePzeaUUbEHE=
github.com/google/go-github/v69 v69.2.0/go.mod h1:xne4jymxLR6Uj9b7J7PyTpkMYstEMMwGZa0Aehh1azM=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
@@ -362,8 +362,8 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+1 -1
View File
@@ -3,7 +3,7 @@ module github.com/grafana/grafana/pkg/build/wire
go 1.23.1
require (
github.com/google/go-cmp v0.6.0
github.com/google/go-cmp v0.7.0
github.com/google/subcommands v1.2.0
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2
golang.org/x/tools v0.29.0
+2 -2
View File
@@ -1,5 +1,5 @@
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE=
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+1 -1
View File
@@ -21,7 +21,7 @@ require (
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/golang/glog v1.2.4 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
+2 -2
View File
@@ -25,8 +25,8 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU
github.com/golang/glog v1.2.4 h1:CNNw5U8lSiiBk7druxtSHHTsRWcxKoac6kZKm2peBBc=
github.com/golang/glog v1.2.4/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
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/codejen v0.0.4-0.20230321061741-77f656893a3d h1:hrXbGJ5jgp6yNITzs5o+zXq0V5yT3siNJ+uM8LGwWKk=
+2 -2
View File
@@ -1,5 +1,5 @@
//go:build enterprise
// +build enterprise
//go:build enterprise || pro
// +build enterprise pro
package extensions
+1 -1
View File
@@ -21,7 +21,7 @@ require (
github.com/getkin/kin-openapi v0.129.0 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
+2 -2
View File
@@ -24,8 +24,8 @@ github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
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/codejen v0.0.4-0.20230321061741-77f656893a3d h1:hrXbGJ5jgp6yNITzs5o+zXq0V5yT3siNJ+uM8LGwWKk=
+4 -4
View File
@@ -6,7 +6,7 @@ toolchain go1.23.6
require (
github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040
github.com/grafana/grafana-plugin-sdk-go v0.266.0
github.com/grafana/grafana-plugin-sdk-go v0.267.0
github.com/json-iterator/go v1.1.12
github.com/prometheus/client_golang v1.20.5
github.com/prometheus/common v0.62.0
@@ -33,7 +33,7 @@ require (
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dennwc/varint v1.0.0 // indirect
github.com/elazarl/goproxy v1.7.0 // indirect
github.com/elazarl/goproxy v1.7.1 // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/fatih/color v1.17.0 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
@@ -48,7 +48,7 @@ require (
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/flatbuffers v24.3.25+incompatible // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/mux v1.8.1 // indirect
@@ -121,7 +121,7 @@ require (
google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
google.golang.org/grpc v1.70.0 // indirect
google.golang.org/protobuf v1.36.4 // indirect
google.golang.org/protobuf v1.36.5 // indirect
gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
+8 -7
View File
@@ -55,8 +55,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dennwc/varint v1.0.0 h1:kGNFFSSw8ToIy3obO/kKr8U9GZYUAxQEVuix4zfDWzE=
github.com/dennwc/varint v1.0.0/go.mod h1:hnItb35rvZvJrbTALZtY/iQfDs48JKRG1RPpgziApxA=
github.com/elazarl/goproxy v1.7.0 h1:EXv2nV4EjM60ZtsEVLYJG4oBXhDGutMKperpHsZ/v+0=
github.com/elazarl/goproxy v1.7.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ=
github.com/elazarl/goproxy v1.7.1 h1:1P7LPSxbqtNxusFnXclj6O56pjfq1xOQZ6a0mwwKUlY=
github.com/elazarl/goproxy v1.7.1/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
@@ -100,8 +100,9 @@ github.com/google/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZat
github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -120,8 +121,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040 h1:IR+UNYHqaU31t8/TArJk8K/GlDwOyxMpGNkWCXeZ28g=
github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040/go.mod h1:SPLNCARd4xdjCkue0O6hvuoveuS1dGJjDnfxYe405YQ=
github.com/grafana/grafana-plugin-sdk-go v0.266.0 h1:YP+iEpXH3HRX9Xo4NHjsrJhN2W7uVTtkLNzMHYbmiLI=
github.com/grafana/grafana-plugin-sdk-go v0.266.0/go.mod h1:bxkXrBQ4QSmOncsWdIOcpgP+M6wajQNMAPXlbWrqAWY=
github.com/grafana/grafana-plugin-sdk-go v0.267.0 h1:4AcuLEE0UeFG0Eo3P8r1FDS89hgZZ73K550DRz1fgjQ=
github.com/grafana/grafana-plugin-sdk-go v0.267.0/go.mod h1:OuwS4c/JYgn0rr/w5zhJBpLo4gKm/vw15RsfpYAvK9Q=
github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8=
github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls=
github.com/grafana/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKtUuKQbJqgAIjlnicKg=
@@ -394,8 +395,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@@ -13,7 +13,7 @@ import (
model "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/resource/receiver/v0alpha1"
"github.com/grafana/grafana/pkg/apimachinery/identity"
grafanaRest "github.com/grafana/grafana/pkg/apiserver/rest"
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
alertingac "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
@@ -22,7 +22,7 @@ import (
)
var (
_ grafanaRest.LegacyStorage = (*legacyStorage)(nil)
_ grafanarest.Storage = (*legacyStorage)(nil)
)
type ReceiverService interface {
@@ -11,14 +11,14 @@ import (
"k8s.io/apiserver/pkg/registry/rest"
model "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/resource/routingtree/v0alpha1"
grafanaRest "github.com/grafana/grafana/pkg/apiserver/rest"
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
alerting_models "github.com/grafana/grafana/pkg/services/ngalert/models"
)
var (
_ grafanaRest.LegacyStorage = (*legacyStorage)(nil)
_ grafanarest.Storage = (*legacyStorage)(nil)
)
type RouteService interface {
@@ -4,13 +4,14 @@ import (
"context"
"fmt"
"github.com/grafana/alerting/templates"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/registry/rest"
"github.com/grafana/alerting/templates"
model "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/resource/templategroup/v0alpha1"
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
@@ -19,7 +20,7 @@ import (
)
var (
_ grafanarest.LegacyStorage = (*legacyStorage)(nil)
_ grafanarest.Storage = (*legacyStorage)(nil)
)
type TemplateService interface {
@@ -11,14 +11,14 @@ import (
"k8s.io/apiserver/pkg/registry/rest"
model "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/resource/timeinterval/v0alpha1"
grafanaRest "github.com/grafana/grafana/pkg/apiserver/rest"
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
)
var (
_ grafanaRest.LegacyStorage = (*legacyStorage)(nil)
_ grafanarest.Storage = (*legacyStorage)(nil)
)
type TimeIntervalService interface {
@@ -28,7 +28,7 @@ type DashboardStorage struct {
Features featuremgmt.FeatureToggles
}
func (s *DashboardStorage) NewStore(scheme *runtime.Scheme, defaultOptsGetter generic.RESTOptionsGetter, reg prometheus.Registerer) (grafanarest.LegacyStorage, error) {
func (s *DashboardStorage) NewStore(scheme *runtime.Scheme, defaultOptsGetter generic.RESTOptionsGetter, reg prometheus.Registerer) (grafanarest.Storage, error) {
server, err := resource.NewResourceServer(resource.ResourceServerOptions{
Backend: s.Access,
Reg: reg,
+3 -2
View File
@@ -17,6 +17,8 @@ import (
"k8s.io/kube-openapi/pkg/spec3"
"k8s.io/kube-openapi/pkg/validation/spec"
"github.com/prometheus/client_golang/prometheus"
claims "github.com/grafana/authlib/types"
"github.com/grafana/grafana/pkg/apimachinery/utils"
dashboardinternal "github.com/grafana/grafana/pkg/apis/dashboard"
@@ -42,7 +44,6 @@ import (
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
"github.com/grafana/grafana/pkg/storage/unified/apistore"
"github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/prometheus/client_golang/prometheus"
)
var (
@@ -247,7 +248,7 @@ func (b *DashboardsAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver
func (b *DashboardsAPIBuilder) storageForVersion(
opts builder.APIGroupOptions,
legacyStore grafanarest.LegacyStorage,
legacyStore grafanarest.Storage,
largeObjects apistore.LargeObjectSupport,
newDTOFunc func() runtime.Object,
) (map[string]rest.Storage, error) {
+1 -1
View File
@@ -116,7 +116,7 @@ func (s *legacyStorage) List(ctx context.Context, options *internalversion.ListO
}
list.Items = append(list.Items, *r)
}
if len(list.Items) >= int(paging.limit) {
if int64(len(list.Items)) >= paging.limit {
list.Continue = paging.GetNextPageToken()
}
return list, nil
+1 -1
View File
@@ -48,7 +48,7 @@ func RegisterApp(
return provider
}
func (p *PlaylistAppProvider) legacyStorageGetter(requested schema.GroupVersionResource) grafanarest.LegacyStorage {
func (p *PlaylistAppProvider) legacyStorageGetter(requested schema.GroupVersionResource) grafanarest.Storage {
gvr := schema.GroupVersionResource{
Group: playlistv0alpha1.PlaylistKind().Group(),
Version: playlistv0alpha1.PlaylistKind().Version(),
+1
View File
@@ -6,5 +6,6 @@ require go.opentelemetry.io/otel v1.34.0
require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
)
+2 -2
View File
@@ -1,7 +1,7 @@
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
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/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+1 -1
View File
@@ -286,7 +286,7 @@ func InstallAPIs(
// nolint:staticcheck
if storageOpts.StorageType != options.StorageTypeLegacy {
dualWrite = func(gr schema.GroupResource, legacy grafanarest.LegacyStorage, storage grafanarest.Storage) (grafanarest.Storage, error) {
dualWrite = func(gr schema.GroupResource, legacy grafanarest.Storage, storage grafanarest.Storage) (grafanarest.Storage, error) {
// Dashboards + Folders may be managed (depends on feature toggles and database state)
if dualWriteService != nil && dualWriteService.ShouldManage(gr) {
return dualWriteService.NewStorage(gr, legacy, storage) // eventually this can replace this whole function
@@ -18,7 +18,7 @@ import (
var _ AppBuilder = (*appBuilder)(nil)
type LegacyStorageGetter func(schema.GroupVersionResource) grafanarest.LegacyStorage
type LegacyStorageGetter func(schema.GroupVersionResource) grafanarest.Storage
type AppBuilderConfig struct {
Authorizer authorizer.Authorizer
@@ -29,7 +29,7 @@ import (
func ProvideService(cfg *setting.Cfg, authenticator authn.Authenticator, features featuremgmt.FeatureToggles,
) *ContextHandler {
return &ContextHandler{
Cfg: cfg,
cfg: cfg,
authenticator: authenticator,
features: features,
}
@@ -37,7 +37,7 @@ func ProvideService(cfg *setting.Cfg, authenticator authn.Authenticator, feature
// ContextHandler is a middleware.
type ContextHandler struct {
Cfg *setting.Cfg
cfg *setting.Cfg
authenticator authn.Authenticator
features featuremgmt.FeatureToggles
}
@@ -104,7 +104,7 @@ func (h *ContextHandler) Middleware(next http.Handler) http.Handler {
// inject ReqContext in the context
ctx = context.WithValue(ctx, reqContextKey{}, reqContext)
// store list of possible auth header in context
ctx = WithAuthHTTPHeaders(ctx, h.Cfg)
ctx = WithAuthHTTPHeaders(ctx, h.cfg)
// Set the context for the http.Request.Context
// This modifies both r and reqContext.Req since they point to the same value
*reqContext.Req = *reqContext.Req.WithContext(ctx)
@@ -137,7 +137,7 @@ func (h *ContextHandler) Middleware(next http.Handler) http.Handler {
attribute.Int64("userId", reqContext.UserID),
))
if h.Cfg.IDResponseHeaderEnabled && reqContext.SignedInUser != nil {
if h.cfg.IDResponseHeaderEnabled && reqContext.SignedInUser != nil {
reqContext.Resp.Before(h.addIDHeaderEndOfRequestFunc(reqContext.SignedInUser))
}
@@ -167,11 +167,11 @@ func (h *ContextHandler) addIDHeaderEndOfRequestFunc(ident identity.Requester) w
return
}
if _, ok := h.Cfg.IDResponseHeaderNamespaces[string(ident.GetIdentityType())]; !ok {
if _, ok := h.cfg.IDResponseHeaderNamespaces[string(ident.GetIdentityType())]; !ok {
return
}
headerName := fmt.Sprintf("%s-Identity-Id", h.Cfg.IDResponseHeaderPrefix)
headerName := fmt.Sprintf("%s-Identity-Id", h.cfg.IDResponseHeaderPrefix)
w.Header().Add(headerName, ident.GetID())
}
}
@@ -334,10 +334,9 @@ func getRestoreVersion(msg string) (int, error) {
return 0, nil
}
ver, err := strconv.ParseInt(parts[1], 10, 64)
ver, err := strconv.Atoi(parts[1])
if err != nil {
return 0, err
}
return int(ver), nil
return ver, nil
}
@@ -103,12 +103,46 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetRules(c *contextmodel.
// RouteConvertPrometheusDeleteNamespace deletes all rule groups that were imported from a Prometheus-compatible source
// within a specified namespace.
func (srv *ConvertPrometheusSrv) RouteConvertPrometheusDeleteNamespace(c *contextmodel.ReqContext, namespaceTitle string) response.Response {
return response.Error(501, "Not implemented", nil)
logger := srv.logger.FromContext(c.Req.Context())
logger.Debug("Looking up folder in the root by title", "folder_title", namespaceTitle)
namespace, err := srv.ruleStore.GetNamespaceInRootByTitle(c.Req.Context(), namespaceTitle, c.SignedInUser.GetOrgID(), c.SignedInUser)
if err != nil {
return namespaceErrorResponse(err)
}
logger.Info("Deleting all Prometheus-imported rule groups", "folder_uid", namespace.UID, "folder_title", namespaceTitle)
filterOpts := &provisioning.FilterOptions{
NamespaceUIDs: []string{namespace.UID},
ImportedPrometheusRule: util.Pointer(true),
}
err = srv.alertRuleService.DeleteRuleGroups(c.Req.Context(), c.SignedInUser, models.ProvenanceConvertedPrometheus, filterOpts)
if err != nil {
logger.Error("Failed to delete rule groups", "folder_uid", namespace.UID, "error", err)
return errorToResponse(err)
}
return successfulResponse()
}
// RouteConvertPrometheusDeleteRuleGroup deletes a specific rule group if it was imported from a Prometheus-compatible source.
func (srv *ConvertPrometheusSrv) RouteConvertPrometheusDeleteRuleGroup(c *contextmodel.ReqContext, namespaceTitle string, group string) response.Response {
return response.Error(501, "Not implemented", nil)
logger := srv.logger.FromContext(c.Req.Context())
logger.Debug("Looking up folder in the root by title", "folder_title", namespaceTitle)
folder, err := srv.ruleStore.GetNamespaceInRootByTitle(c.Req.Context(), namespaceTitle, c.SignedInUser.GetOrgID(), c.SignedInUser)
if err != nil {
return namespaceErrorResponse(err)
}
logger.Info("Deleting Prometheus-imported rule group", "folder_uid", folder.UID, "folder_title", namespaceTitle, "group", group)
err = srv.alertRuleService.DeleteRuleGroup(c.Req.Context(), c.SignedInUser, folder.UID, group, models.ProvenanceConvertedPrometheus)
if err != nil {
logger.Error("Failed to delete rule group", "folder_uid", folder.UID, "group", group, "error", err)
return errorToResponse(err)
}
return successfulResponse()
}
// RouteConvertPrometheusGetNamespace returns the Grafana-managed alert rules for a specified namespace (folder).
@@ -220,7 +254,7 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusPostRuleGroup(c *contextm
return errorToResponse(err)
}
return response.JSON(http.StatusAccepted, map[string]string{"status": "success"})
return successfulResponse()
}
func (srv *ConvertPrometheusSrv) getOrCreateNamespace(c *contextmodel.ReqContext, title string, logger log.Logger) (*folder.Folder, response.Response) {
@@ -363,3 +397,9 @@ func namespaceErrorResponse(err error) response.Response {
return toNamespaceErrorResponse(err)
}
func successfulResponse() response.Response {
return response.JSON(http.StatusAccepted, apimodels.ConvertPrometheusResponse{
Status: "success",
})
}
@@ -2,6 +2,7 @@ package api
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"testing"
@@ -77,6 +78,79 @@ func TestRouteConvertPrometheusPostRuleGroup(t *testing.T) {
require.Equal(t, http.StatusAccepted, response.Status())
})
t.Run("should replace an existing rule group", func(t *testing.T) {
provenanceStore := fakes.NewFakeProvisioningStore()
srv, _, ruleStore, folderService := createConvertPrometheusSrv(t, withProvenanceStore(provenanceStore))
// Create a folder in the root
fldr := randFolder()
fldr.ParentUID = ""
folderService.ExpectedFolder = fldr
folderService.ExpectedFolders = []*folder.Folder{fldr}
ruleStore.Folders[1] = append(ruleStore.Folders[1], fldr)
// And a rule
rule := models.RuleGen.
With(models.RuleGen.WithNamespaceUID(fldr.UID)).
With(models.RuleGen.WithGroupName(simpleGroup.Name)).
With(models.RuleGen.WithOrgID(1)).
With(models.RuleGen.WithPrometheusOriginalRuleDefinition("123")).
GenerateRef()
ruleStore.PutRule(context.Background(), rule)
rc := createRequestCtx()
response := srv.RouteConvertPrometheusPostRuleGroup(rc, fldr.Title, simpleGroup)
require.Equal(t, http.StatusAccepted, response.Status())
// Get the updated rule
remaining, err := ruleStore.ListAlertRules(context.Background(), &models.ListAlertRulesQuery{
OrgID: 1,
})
require.NoError(t, err)
require.Len(t, remaining, 1)
require.Equal(t, simpleGroup.Name, remaining[0].RuleGroup)
require.Equal(t, fmt.Sprintf("[%s] %s", simpleGroup.Name, simpleGroup.Rules[0].Alert), remaining[0].Title)
promRuleYAML, err := yaml.Marshal(simpleGroup.Rules[0])
require.NoError(t, err)
require.Equal(t, string(promRuleYAML), remaining[0].PrometheusRuleDefinition())
})
t.Run("should fail to replace a provisioned rule group", func(t *testing.T) {
provenanceStore := fakes.NewFakeProvisioningStore()
srv, _, ruleStore, folderService := createConvertPrometheusSrv(t, withProvenanceStore(provenanceStore))
// Create a folder in the root
fldr := randFolder()
fldr.ParentUID = ""
folderService.ExpectedFolder = fldr
folderService.ExpectedFolders = []*folder.Folder{fldr}
ruleStore.Folders[1] = append(ruleStore.Folders[1], fldr)
rule := models.RuleGen.
With(models.RuleGen.WithNamespaceUID(fldr.UID)).
With(models.RuleGen.WithGroupName(simpleGroup.Name)).
With(models.RuleGen.WithOrgID(1)).
With(models.RuleGen.WithPrometheusOriginalRuleDefinition("123")).
GenerateRef()
ruleStore.PutRule(context.Background(), rule)
// mark the rule as provisioned
err := provenanceStore.SetProvenance(context.Background(), rule, 1, models.ProvenanceAPI)
require.NoError(t, err)
rc := createRequestCtx()
response := srv.RouteConvertPrometheusPostRuleGroup(rc, fldr.Title, simpleGroup)
require.Equal(t, http.StatusConflict, response.Status())
// Verify the rule is still present
remaining, err := ruleStore.GetAlertRuleByUID(context.Background(), &models.GetAlertRuleByUIDQuery{
UID: rule.UID,
OrgID: rule.OrgID,
})
require.NoError(t, err)
require.NotNil(t, remaining)
})
t.Run("with valid pause header values should return 202", func(t *testing.T) {
testCases := []struct {
name string
@@ -420,9 +494,219 @@ func TestRouteConvertPrometheusGetRules(t *testing.T) {
})
}
func createConvertPrometheusSrv(t *testing.T) (*ConvertPrometheusSrv, datasources.CacheService, *fakes.RuleStore, *foldertest.FakeService) {
func TestRouteConvertPrometheusDeleteNamespace(t *testing.T) {
t.Run("for non-existent folder should return 404", func(t *testing.T) {
srv, _, _, _ := createConvertPrometheusSrv(t)
rc := createRequestCtx()
response := srv.RouteConvertPrometheusDeleteNamespace(rc, "non-existent")
require.Equal(t, http.StatusNotFound, response.Status())
})
t.Run("valid request should delete rules", func(t *testing.T) {
initNamespace := func(promDefinition string, opts ...convertPrometheusSrvOptionsFunc) (*ConvertPrometheusSrv, *fakes.RuleStore, *folder.Folder, *models.AlertRule) {
srv, _, ruleStore, folderService := createConvertPrometheusSrv(t, opts...)
// Create a folder in the root
fldr := randFolder()
fldr.ParentUID = ""
folderService.ExpectedFolder = fldr
folderService.ExpectedFolders = []*folder.Folder{fldr}
ruleStore.Folders[1] = append(ruleStore.Folders[1], fldr)
rule := models.RuleGen.
With(models.RuleGen.WithNamespaceUID(fldr.UID)).
With(models.RuleGen.WithOrgID(1)).
With(models.RuleGen.WithPrometheusOriginalRuleDefinition(promDefinition)).
GenerateRef()
ruleStore.PutRule(context.Background(), rule)
return srv, ruleStore, fldr, rule
}
t.Run("valid request should delete rules", func(t *testing.T) {
srv, ruleStore, fldr, rule := initNamespace("prometheus definition")
// Create another rule group in a different namespace that should not be deleted
otherGroupName := "other-group"
otherRule := models.RuleGen.
With(models.RuleGen.WithOrgID(1)).
With(models.RuleGen.WithGroupName(otherGroupName)).
With(models.RuleGen.WithPrometheusOriginalRuleDefinition("other prometheus definition")).
GenerateRef()
ruleStore.PutRule(context.Background(), otherRule)
rc := createRequestCtx()
response := srv.RouteConvertPrometheusDeleteNamespace(rc, fldr.Title)
require.Equal(t, http.StatusAccepted, response.Status())
// Verify the rule in the specified group was deleted
remaining, err := ruleStore.GetAlertRuleByUID(context.Background(), &models.GetAlertRuleByUIDQuery{
UID: rule.UID,
OrgID: rule.OrgID,
})
require.Error(t, err)
require.Nil(t, remaining)
// Verify the rule in the other group still exists
remainingOther, err := ruleStore.GetAlertRuleByUID(context.Background(), &models.GetAlertRuleByUIDQuery{
UID: otherRule.UID,
OrgID: otherRule.OrgID,
})
require.NoError(t, err)
require.NotNil(t, remainingOther)
})
t.Run("fails to delete rules when they are provisioned", func(t *testing.T) {
provenanceStore := fakes.NewFakeProvisioningStore()
srv, ruleStore, fldr, rule := initNamespace("", withProvenanceStore(provenanceStore))
rc := createRequestCtx()
// Create a provisioned rule
rule2 := models.RuleGen.
With(models.RuleGen.WithNamespaceUID(fldr.UID)).
With(models.RuleGen.WithOrgID(1)).
With(models.RuleGen.WithPrometheusOriginalRuleDefinition("prometheus definition")).
GenerateRef()
ruleStore.PutRule(context.Background(), rule2)
err := provenanceStore.SetProvenance(context.Background(), rule2, 1, models.ProvenanceAPI)
require.NoError(t, err)
response := srv.RouteConvertPrometheusDeleteNamespace(rc, fldr.Title)
require.Equal(t, http.StatusConflict, response.Status())
// Verify the rule is still present
remaining, err := ruleStore.GetAlertRuleByUID(context.Background(), &models.GetAlertRuleByUIDQuery{
UID: rule.UID,
OrgID: rule.OrgID,
})
require.NoError(t, err)
require.NotNil(t, remaining)
})
})
}
func TestRouteConvertPrometheusDeleteRuleGroup(t *testing.T) {
t.Run("for non-existent folder should return 404", func(t *testing.T) {
srv, _, _, _ := createConvertPrometheusSrv(t)
rc := createRequestCtx()
response := srv.RouteConvertPrometheusDeleteRuleGroup(rc, "non-existent", "test-group")
require.Equal(t, http.StatusNotFound, response.Status())
})
const groupName = "test-group"
t.Run("valid request should delete rules", func(t *testing.T) {
initGroup := func(promDefinition string, groupName string, opts ...convertPrometheusSrvOptionsFunc) (*ConvertPrometheusSrv, *fakes.RuleStore, *folder.Folder, *models.AlertRule) {
srv, _, ruleStore, folderService := createConvertPrometheusSrv(t, opts...)
// Create a folder in the root
fldr := randFolder()
fldr.ParentUID = ""
folderService.ExpectedFolder = fldr
folderService.ExpectedFolders = []*folder.Folder{fldr}
ruleStore.Folders[1] = append(ruleStore.Folders[1], fldr)
rule := models.RuleGen.
With(models.RuleGen.WithNamespaceUID(fldr.UID)).
With(models.RuleGen.WithOrgID(1)).
With(models.RuleGen.WithGroupName(groupName)).
With(models.RuleGen.WithPrometheusOriginalRuleDefinition(promDefinition)).
GenerateRef()
ruleStore.PutRule(context.Background(), rule)
return srv, ruleStore, fldr, rule
}
t.Run("valid request should delete rules", func(t *testing.T) {
srv, ruleStore, fldr, rule := initGroup("prometheus definition", groupName)
rc := createRequestCtx()
// Create another rule in a different group that should not be deleted
otherGroupName := "other-group"
otherRule := models.RuleGen.
With(models.RuleGen.WithNamespaceUID(fldr.UID)).
With(models.RuleGen.WithOrgID(1)).
With(models.RuleGen.WithGroupName(otherGroupName)).
With(models.RuleGen.WithPrometheusOriginalRuleDefinition("other prometheus definition")).
GenerateRef()
ruleStore.PutRule(context.Background(), otherRule)
response := srv.RouteConvertPrometheusDeleteRuleGroup(rc, fldr.Title, groupName)
require.Equal(t, http.StatusAccepted, response.Status())
// Verify the rule was deleted
remaining, err := ruleStore.GetAlertRuleByUID(context.Background(), &models.GetAlertRuleByUIDQuery{
UID: rule.UID,
OrgID: rule.OrgID,
})
require.Error(t, err)
require.Nil(t, remaining)
// Verify the otherRule from the "other-group" is still present
otherRuleRefreshed, err := ruleStore.GetAlertRuleByUID(context.Background(), &models.GetAlertRuleByUIDQuery{
UID: otherRule.UID,
OrgID: otherRule.OrgID,
})
require.NoError(t, err)
require.NotNil(t, otherRuleRefreshed)
})
t.Run("fails to delete rules when they are provisioned", func(t *testing.T) {
provenanceStore := fakes.NewFakeProvisioningStore()
srv, ruleStore, fldr, rule := initGroup("", groupName, withProvenanceStore(provenanceStore))
rc := createRequestCtx()
// Create a provisioned rule
rule2 := models.RuleGen.
With(models.RuleGen.WithNamespaceUID(fldr.UID)).
With(models.RuleGen.WithOrgID(1)).
With(models.RuleGen.WithGroupName(groupName)).
With(models.RuleGen.WithPrometheusOriginalRuleDefinition("prometheus definition")).
GenerateRef()
ruleStore.PutRule(context.Background(), rule2)
err := provenanceStore.SetProvenance(context.Background(), rule2, 1, models.ProvenanceAPI)
require.NoError(t, err)
response := srv.RouteConvertPrometheusDeleteRuleGroup(rc, fldr.Title, groupName)
require.Equal(t, http.StatusConflict, response.Status())
// Verify the rule is still present
remaining, err := ruleStore.GetAlertRuleByUID(context.Background(), &models.GetAlertRuleByUIDQuery{
UID: rule.UID,
OrgID: rule.OrgID,
})
require.NoError(t, err)
require.NotNil(t, remaining)
})
})
}
type convertPrometheusSrvOptions struct {
provenanceStore provisioning.ProvisioningStore
}
type convertPrometheusSrvOptionsFunc func(*convertPrometheusSrvOptions)
func withProvenanceStore(store provisioning.ProvisioningStore) convertPrometheusSrvOptionsFunc {
return func(opts *convertPrometheusSrvOptions) {
opts.provenanceStore = store
}
}
func createConvertPrometheusSrv(t *testing.T, opts ...convertPrometheusSrvOptionsFunc) (*ConvertPrometheusSrv, datasources.CacheService, *fakes.RuleStore, *foldertest.FakeService) {
t.Helper()
options := convertPrometheusSrvOptions{
provenanceStore: fakes.NewFakeProvisioningStore(),
}
for _, opt := range opts {
opt(&options)
}
ruleStore := fakes.NewRuleStore(t)
folder := randFolder()
ruleStore.Folders[1] = append(ruleStore.Folders[1], folder)
@@ -441,7 +725,7 @@ func createConvertPrometheusSrv(t *testing.T) (*ConvertPrometheusSrv, datasource
alertRuleService := provisioning.NewAlertRuleService(
ruleStore,
fakes.NewFakeProvisioningStore(),
options.provenanceStore,
folderService,
quotas,
&provisioning.NopTransactionManager{},
@@ -75,6 +75,7 @@ type AlertRuleService interface {
GetRuleGroup(ctx context.Context, user identity.Requester, folder, group string) (alerting_models.AlertRuleGroup, error)
ReplaceRuleGroup(ctx context.Context, user identity.Requester, group alerting_models.AlertRuleGroup, provenance alerting_models.Provenance) error
DeleteRuleGroup(ctx context.Context, user identity.Requester, folder, group string, provenance alerting_models.Provenance) error
DeleteRuleGroups(ctx context.Context, user identity.Requester, provenance alerting_models.Provenance, opts *provisioning.FilterOptions) error
GetAlertRuleWithFolderFullpath(ctx context.Context, u identity.Requester, ruleUID string) (provisioning.AlertRuleWithFolderFullpath, error)
GetAlertRuleGroupWithFolderFullpath(ctx context.Context, u identity.Requester, folder, group string) (alerting_models.AlertRuleGroupWithFolderFullpath, error)
GetAlertGroupsWithFolderFullpath(ctx context.Context, u identity.Requester, opts *provisioning.FilterOptions) ([]alerting_models.AlertRuleGroupWithFolderFullpath, error)
+10 -4
View File
@@ -125,26 +125,32 @@ func (api *API) authorize(method, path string) web.Handler {
// convert/prometheus API paths
case http.MethodGet + "/api/convert/prometheus/config/v1/rules/{NamespaceTitle}/{Group}",
http.MethodGet + "/api/convert/prometheus/config/v1/rules/{NamespaceTitle}":
http.MethodGet + "/api/convert/api/prom/rules/{NamespaceTitle}/{Group}",
http.MethodGet + "/api/convert/prometheus/config/v1/rules/{NamespaceTitle}",
http.MethodGet + "/api/convert/api/prom/rules/{NamespaceTitle}":
eval = ac.EvalAll(
ac.EvalPermission(ac.ActionAlertingRuleRead),
ac.EvalPermission(dashboards.ActionFoldersRead),
)
case http.MethodGet + "/api/convert/prometheus/config/v1/rules":
case http.MethodGet + "/api/convert/prometheus/config/v1/rules",
http.MethodGet + "/api/convert/api/prom/rules":
eval = ac.EvalAll(
ac.EvalPermission(ac.ActionAlertingRuleRead),
ac.EvalPermission(dashboards.ActionFoldersRead),
)
case http.MethodPost + "/api/convert/prometheus/config/v1/rules/{NamespaceTitle}":
case http.MethodPost + "/api/convert/prometheus/config/v1/rules/{NamespaceTitle}",
http.MethodPost + "/api/convert/api/prom/rules/{NamespaceTitle}":
eval = ac.EvalAll(
ac.EvalPermission(ac.ActionAlertingRuleCreate),
ac.EvalPermission(ac.ActionAlertingProvisioningSetStatus),
)
case http.MethodDelete + "/api/convert/prometheus/config/v1/rules/{NamespaceTitle}/{Group}",
http.MethodDelete + "/api/convert/prometheus/config/v1/rules/{NamespaceTitle}":
http.MethodDelete + "/api/convert/api/prom/rules/{NamespaceTitle}/{Group}",
http.MethodDelete + "/api/convert/prometheus/config/v1/rules/{NamespaceTitle}",
http.MethodDelete + "/api/convert/api/prom/rules/{NamespaceTitle}":
eval = ac.EvalAny(
ac.EvalAll(
ac.EvalPermission(ac.ActionAlertingRuleRead),
@@ -41,7 +41,7 @@ func TestAuthorize(t *testing.T) {
}
paths[p] = methods
}
require.Len(t, paths, 63)
require.Len(t, paths, 66)
ac := acmock.New()
api := &API{AccessControl: ac, FeatureManager: featuremgmt.WithFeatures()}
+3 -2
View File
@@ -3,6 +3,7 @@ package api
import (
"errors"
"fmt"
"net/http"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/apimachinery/errutil"
@@ -33,7 +34,7 @@ func errorToResponse(err error) response.Response {
return response.Err(err)
}
if errors.Is(err, datasources.ErrDataSourceNotFound) {
return ErrResp(404, err, "")
return ErrResp(http.StatusNotFound, err, "")
}
if errors.Is(err, errUnexpectedDatasourceType) {
return ErrResp(400, err, "")
@@ -41,5 +42,5 @@ func errorToResponse(err error) response.Response {
if errors.Is(err, errFolderAccess) {
return toNamespaceErrorResponse(err)
}
return ErrResp(500, err, "")
return ErrResp(http.StatusInternalServerError, err, "")
}
@@ -19,6 +19,12 @@ import (
)
type ConvertPrometheusApi interface {
RouteConvertPrometheusCortexDeleteNamespace(*contextmodel.ReqContext) response.Response
RouteConvertPrometheusCortexDeleteRuleGroup(*contextmodel.ReqContext) response.Response
RouteConvertPrometheusCortexGetNamespace(*contextmodel.ReqContext) response.Response
RouteConvertPrometheusCortexGetRuleGroup(*contextmodel.ReqContext) response.Response
RouteConvertPrometheusCortexGetRules(*contextmodel.ReqContext) response.Response
RouteConvertPrometheusCortexPostRuleGroup(*contextmodel.ReqContext) response.Response
RouteConvertPrometheusDeleteNamespace(*contextmodel.ReqContext) response.Response
RouteConvertPrometheusDeleteRuleGroup(*contextmodel.ReqContext) response.Response
RouteConvertPrometheusGetNamespace(*contextmodel.ReqContext) response.Response
@@ -27,6 +33,36 @@ type ConvertPrometheusApi interface {
RouteConvertPrometheusPostRuleGroup(*contextmodel.ReqContext) response.Response
}
func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusCortexDeleteNamespace(ctx *contextmodel.ReqContext) response.Response {
// Parse Path Parameters
namespaceTitleParam := web.Params(ctx.Req)[":NamespaceTitle"]
return f.handleRouteConvertPrometheusCortexDeleteNamespace(ctx, namespaceTitleParam)
}
func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusCortexDeleteRuleGroup(ctx *contextmodel.ReqContext) response.Response {
// Parse Path Parameters
namespaceTitleParam := web.Params(ctx.Req)[":NamespaceTitle"]
groupParam := web.Params(ctx.Req)[":Group"]
return f.handleRouteConvertPrometheusCortexDeleteRuleGroup(ctx, namespaceTitleParam, groupParam)
}
func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusCortexGetNamespace(ctx *contextmodel.ReqContext) response.Response {
// Parse Path Parameters
namespaceTitleParam := web.Params(ctx.Req)[":NamespaceTitle"]
return f.handleRouteConvertPrometheusCortexGetNamespace(ctx, namespaceTitleParam)
}
func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusCortexGetRuleGroup(ctx *contextmodel.ReqContext) response.Response {
// Parse Path Parameters
namespaceTitleParam := web.Params(ctx.Req)[":NamespaceTitle"]
groupParam := web.Params(ctx.Req)[":Group"]
return f.handleRouteConvertPrometheusCortexGetRuleGroup(ctx, namespaceTitleParam, groupParam)
}
func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusCortexGetRules(ctx *contextmodel.ReqContext) response.Response {
return f.handleRouteConvertPrometheusCortexGetRules(ctx)
}
func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusCortexPostRuleGroup(ctx *contextmodel.ReqContext) response.Response {
// Parse Path Parameters
namespaceTitleParam := web.Params(ctx.Req)[":NamespaceTitle"]
return f.handleRouteConvertPrometheusCortexPostRuleGroup(ctx, namespaceTitleParam)
}
func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusDeleteNamespace(ctx *contextmodel.ReqContext) response.Response {
// Parse Path Parameters
namespaceTitleParam := web.Params(ctx.Req)[":NamespaceTitle"]
@@ -60,6 +96,78 @@ func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusPostRuleGroup(ctx *c
func (api *API) RegisterConvertPrometheusApiEndpoints(srv ConvertPrometheusApi, m *metrics.API) {
api.RouteRegister.Group("", func(group routing.RouteRegister) {
group.Delete(
toMacaronPath("/api/convert/api/prom/rules/{NamespaceTitle}"),
requestmeta.SetOwner(requestmeta.TeamAlerting),
requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow),
api.authorize(http.MethodDelete, "/api/convert/api/prom/rules/{NamespaceTitle}"),
metrics.Instrument(
http.MethodDelete,
"/api/convert/api/prom/rules/{NamespaceTitle}",
api.Hooks.Wrap(srv.RouteConvertPrometheusCortexDeleteNamespace),
m,
),
)
group.Delete(
toMacaronPath("/api/convert/api/prom/rules/{NamespaceTitle}/{Group}"),
requestmeta.SetOwner(requestmeta.TeamAlerting),
requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow),
api.authorize(http.MethodDelete, "/api/convert/api/prom/rules/{NamespaceTitle}/{Group}"),
metrics.Instrument(
http.MethodDelete,
"/api/convert/api/prom/rules/{NamespaceTitle}/{Group}",
api.Hooks.Wrap(srv.RouteConvertPrometheusCortexDeleteRuleGroup),
m,
),
)
group.Get(
toMacaronPath("/api/convert/api/prom/rules/{NamespaceTitle}"),
requestmeta.SetOwner(requestmeta.TeamAlerting),
requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow),
api.authorize(http.MethodGet, "/api/convert/api/prom/rules/{NamespaceTitle}"),
metrics.Instrument(
http.MethodGet,
"/api/convert/api/prom/rules/{NamespaceTitle}",
api.Hooks.Wrap(srv.RouteConvertPrometheusCortexGetNamespace),
m,
),
)
group.Get(
toMacaronPath("/api/convert/api/prom/rules/{NamespaceTitle}/{Group}"),
requestmeta.SetOwner(requestmeta.TeamAlerting),
requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow),
api.authorize(http.MethodGet, "/api/convert/api/prom/rules/{NamespaceTitle}/{Group}"),
metrics.Instrument(
http.MethodGet,
"/api/convert/api/prom/rules/{NamespaceTitle}/{Group}",
api.Hooks.Wrap(srv.RouteConvertPrometheusCortexGetRuleGroup),
m,
),
)
group.Get(
toMacaronPath("/api/convert/api/prom/rules"),
requestmeta.SetOwner(requestmeta.TeamAlerting),
requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow),
api.authorize(http.MethodGet, "/api/convert/api/prom/rules"),
metrics.Instrument(
http.MethodGet,
"/api/convert/api/prom/rules",
api.Hooks.Wrap(srv.RouteConvertPrometheusCortexGetRules),
m,
),
)
group.Post(
toMacaronPath("/api/convert/api/prom/rules/{NamespaceTitle}"),
requestmeta.SetOwner(requestmeta.TeamAlerting),
requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow),
api.authorize(http.MethodPost, "/api/convert/api/prom/rules/{NamespaceTitle}"),
metrics.Instrument(
http.MethodPost,
"/api/convert/api/prom/rules/{NamespaceTitle}",
api.Hooks.Wrap(srv.RouteConvertPrometheusCortexPostRuleGroup),
m,
),
)
group.Delete(
toMacaronPath("/api/convert/prometheus/config/v1/rules/{NamespaceTitle}"),
requestmeta.SetOwner(requestmeta.TeamAlerting),
@@ -20,6 +20,7 @@ func NewConvertPrometheusApi(svc *ConvertPrometheusSrv) *ConvertPrometheusApiHan
}
}
// mimirtool
func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusGetRules(ctx *contextmodel.ReqContext) response.Response {
return f.svc.RouteConvertPrometheusGetRules(ctx)
}
@@ -54,3 +55,28 @@ func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusPostRuleGroup(
return f.svc.RouteConvertPrometheusPostRuleGroup(ctx, namespaceTitle, promGroup)
}
// cortextool
func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusCortexGetRules(ctx *contextmodel.ReqContext) response.Response {
return f.handleRouteConvertPrometheusGetRules(ctx)
}
func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusCortexDeleteNamespace(ctx *contextmodel.ReqContext, namespaceTitle string) response.Response {
return f.handleRouteConvertPrometheusDeleteNamespace(ctx, namespaceTitle)
}
func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusCortexDeleteRuleGroup(ctx *contextmodel.ReqContext, namespaceTitle string, group string) response.Response {
return f.handleRouteConvertPrometheusDeleteRuleGroup(ctx, namespaceTitle, group)
}
func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusCortexGetNamespace(ctx *contextmodel.ReqContext, namespaceTitle string) response.Response {
return f.handleRouteConvertPrometheusGetNamespace(ctx, namespaceTitle)
}
func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusCortexGetRuleGroup(ctx *contextmodel.ReqContext, namespaceTitle string, group string) response.Response {
return f.handleRouteConvertPrometheusGetRuleGroup(ctx, namespaceTitle, group)
}
func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusCortexPostRuleGroup(ctx *contextmodel.ReqContext, namespaceTitle string) response.Response {
return f.handleRouteConvertPrometheusPostRuleGroup(ctx, namespaceTitle)
}
+1 -2
View File
@@ -4493,7 +4493,6 @@
"type": "object"
},
"URL": {
"description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nThe Host field contains the host and port subcomponents of the URL.\nWhen the port is present, it is separated from the host with a colon.\nWhen the host is an IPv6 address, it must be enclosed in square brackets:\n\"[fe80::1]:80\". The [net.JoinHostPort] function combines a host and port\ninto a string suitable for the Host field, adding square brackets to\nthe host when necessary.\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use the [URL.EscapedPath] method, which preserves\nthe original encoding of Path.\n\nThe RawPath field is an optional field which is only set when the default\nencoding of Path is different from the escaped path. See the EscapedPath method\nfor more details.\n\nURL's String method uses the EscapedPath method to obtain the path.",
"properties": {
"ForceQuery": {
"type": "boolean"
@@ -4529,7 +4528,7 @@
"$ref": "#/definitions/Userinfo"
}
},
"title": "A URL represents a parsed URL (technically, a URI reference).",
"title": "URL is a custom URL type that allows validation at configuration load time.",
"type": "object"
},
"UpdateRuleGroupResponse": {
@@ -4,6 +4,7 @@ import (
"github.com/prometheus/common/model"
)
// Route for mimirtool
// swagger:route GET /convert/prometheus/config/v1/rules convert_prometheus RouteConvertPrometheusGetRules
//
// Gets all Grafana-managed alert rules that were imported from Prometheus-compatible sources, grouped by namespace.
@@ -16,6 +17,20 @@ import (
// 403: ForbiddenError
// 404: NotFound
// Route for cortextool
// swagger:route GET /convert/api/prom/rules convert_prometheus RouteConvertPrometheusCortexGetRules
//
// Gets all Grafana-managed alert rules that were imported from Prometheus-compatible sources, grouped by namespace.
//
// Produces:
// - application/yaml
//
// Responses:
// 200: PrometheusNamespace
// 403: ForbiddenError
// 404: NotFound
// Route for mimirtool
// swagger:route GET /convert/prometheus/config/v1/rules/{NamespaceTitle} convert_prometheus RouteConvertPrometheusGetNamespace
//
// Gets Grafana-managed alert rules that were imported from Prometheus-compatible sources for a specified namespace (folder).
@@ -28,6 +43,20 @@ import (
// 403: ForbiddenError
// 404: NotFound
// Route for cortextool
// swagger:route GET /convert/api/prom/rules/{NamespaceTitle} convert_prometheus RouteConvertPrometheusCortexGetNamespace
//
// Gets Grafana-managed alert rules that were imported from Prometheus-compatible sources for a specified namespace (folder).
//
// Produces:
// - application/yaml
//
// Responses:
// 200: PrometheusNamespace
// 403: ForbiddenError
// 404: NotFound
// Route for mimirtool
// swagger:route GET /convert/prometheus/config/v1/rules/{NamespaceTitle}/{Group} convert_prometheus RouteConvertPrometheusGetRuleGroup
//
// Gets a single rule group in Prometheus-compatible format if it was imported from a Prometheus-compatible source.
@@ -40,6 +69,20 @@ import (
// 403: ForbiddenError
// 404: NotFound
// Route for cortextool
// swagger:route GET /convert/api/prom/rules/{NamespaceTitle}/{Group} convert_prometheus RouteConvertPrometheusCortexGetRuleGroup
//
// Gets a single rule group in Prometheus-compatible format if it was imported from a Prometheus-compatible source.
//
// Produces:
// - application/yaml
//
// Responses:
// 200: PrometheusRuleGroup
// 403: ForbiddenError
// 404: NotFound
// Route for mimirtool
// swagger:route POST /convert/prometheus/config/v1/rules/{NamespaceTitle} convert_prometheus RouteConvertPrometheusPostRuleGroup
//
// Converts a Prometheus rule group into a Grafana rule group and creates or updates it within the specified namespace.
@@ -59,6 +102,27 @@ import (
// Extensions:
// x-raw-request: true
// Route for cortextool
// swagger:route POST /convert/api/prom/rules/{NamespaceTitle} convert_prometheus RouteConvertPrometheusCortexPostRuleGroup
//
// Converts a Prometheus rule group into a Grafana rule group and creates or updates it within the specified namespace.
// If the group already exists and was not imported from a Prometheus-compatible source initially,
// it will not be replaced and an error will be returned.
//
// Consumes:
// - application/yaml
//
// Produces:
// - application/json
//
// Responses:
// 202: ConvertPrometheusResponse
// 403: ForbiddenError
//
// Extensions:
// x-raw-request: true
// Route for mimirtool
// swagger:route DELETE /convert/prometheus/config/v1/rules/{NamespaceTitle} convert_prometheus RouteConvertPrometheusDeleteNamespace
//
// Deletes all rule groups that were imported from Prometheus-compatible sources within the specified namespace.
@@ -70,6 +134,19 @@ import (
// 202: ConvertPrometheusResponse
// 403: ForbiddenError
// Route for cortextool
// swagger:route DELETE /convert/api/prom/rules/{NamespaceTitle} convert_prometheus RouteConvertPrometheusCortexDeleteNamespace
//
// Deletes all rule groups that were imported from Prometheus-compatible sources within the specified namespace.
//
// Produces:
// - application/json
//
// Responses:
// 202: ConvertPrometheusResponse
// 403: ForbiddenError
// Route for mimirtool
// swagger:route DELETE /convert/prometheus/config/v1/rules/{NamespaceTitle}/{Group} convert_prometheus RouteConvertPrometheusDeleteRuleGroup
//
// Deletes a specific rule group if it was imported from a Prometheus-compatible source.
@@ -81,7 +158,19 @@ import (
// 202: ConvertPrometheusResponse
// 403: ForbiddenError
// swagger:parameters RouteConvertPrometheusPostRuleGroup
// Route for cortextool
// swagger:route DELETE /convert/api/prom/rules/{NamespaceTitle}/{Group} convert_prometheus RouteConvertPrometheusCortexDeleteRuleGroup
//
// Deletes a specific rule group if it was imported from a Prometheus-compatible source.
//
// Produces:
// - application/json
//
// Responses:
// 202: ConvertPrometheusResponse
// 403: ForbiddenError
// swagger:parameters RouteConvertPrometheusPostRuleGroup RouteConvertPrometheusCortexPostRuleGroup
type RouteConvertPrometheusPostRuleGroupParams struct {
// in: path
NamespaceTitle string
@@ -119,7 +208,7 @@ type PrometheusRule struct {
Record string `yaml:"record,omitempty"`
}
// swagger:parameters RouteConvertPrometheusDeleteRuleGroup RouteConvertPrometheusGetRuleGroup
// swagger:parameters RouteConvertPrometheusDeleteRuleGroup RouteConvertPrometheusCortexDeleteRuleGroup RouteConvertPrometheusGetRuleGroup RouteConvertPrometheusCortexGetRuleGroup
type RouteConvertPrometheusDeleteRuleGroupParams struct {
// in: path
NamespaceTitle string
@@ -127,7 +216,7 @@ type RouteConvertPrometheusDeleteRuleGroupParams struct {
Group string
}
// swagger:parameters RouteConvertPrometheusDeleteNamespace RouteConvertPrometheusGetNamespace
// swagger:parameters RouteConvertPrometheusDeleteNamespace RouteConvertPrometheusCortexDeleteNamespace RouteConvertPrometheusGetNamespace RouteConvertPrometheusCortexGetNamespace
type RouteConvertPrometheusDeleteNamespaceParams struct {
// in: path
NamespaceTitle string
+251 -1
View File
@@ -4493,6 +4493,7 @@
"type": "object"
},
"URL": {
"description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nThe Host field contains the host and port subcomponents of the URL.\nWhen the port is present, it is separated from the host with a colon.\nWhen the host is an IPv6 address, it must be enclosed in square brackets:\n\"[fe80::1]:80\". The [net.JoinHostPort] function combines a host and port\ninto a string suitable for the Host field, adding square brackets to\nthe host when necessary.\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use the [URL.EscapedPath] method, which preserves\nthe original encoding of Path.\n\nThe RawPath field is an optional field which is only set when the default\nencoding of Path is different from the escaped path. See the EscapedPath method\nfor more details.\n\nURL's String method uses the EscapedPath method to obtain the path.",
"properties": {
"ForceQuery": {
"type": "boolean"
@@ -4528,7 +4529,7 @@
"$ref": "#/definitions/Userinfo"
}
},
"title": "URL is a custom URL type that allows validation at configuration load time.",
"title": "A URL represents a parsed URL (technically, a URI reference).",
"type": "object"
},
"UpdateRuleGroupResponse": {
@@ -4770,6 +4771,7 @@
"type": "object"
},
"alertGroups": {
"description": "AlertGroups alert groups",
"items": {
"$ref": "#/definitions/alertGroup",
"type": "object"
@@ -5056,6 +5058,7 @@
"type": "object"
},
"gettableSilences": {
"description": "GettableSilences gettable silences",
"items": {
"$ref": "#/definitions/gettableSilence",
"type": "object"
@@ -6398,6 +6401,253 @@
]
}
},
"/convert/api/prom/rules": {
"get": {
"operationId": "RouteConvertPrometheusCortexGetRules",
"produces": [
"application/yaml"
],
"responses": {
"200": {
"description": "PrometheusNamespace",
"schema": {
"$ref": "#/definitions/PrometheusNamespace"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
},
"404": {
"description": "NotFound",
"schema": {
"$ref": "#/definitions/NotFound"
}
}
},
"summary": "Gets all Grafana-managed alert rules that were imported from Prometheus-compatible sources, grouped by namespace.",
"tags": [
"convert_prometheus"
]
}
},
"/convert/api/prom/rules/{NamespaceTitle}": {
"delete": {
"operationId": "RouteConvertPrometheusCortexDeleteNamespace",
"parameters": [
{
"in": "path",
"name": "NamespaceTitle",
"required": true,
"type": "string"
}
],
"produces": [
"application/json"
],
"responses": {
"202": {
"description": "ConvertPrometheusResponse",
"schema": {
"$ref": "#/definitions/ConvertPrometheusResponse"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
}
},
"summary": "Deletes all rule groups that were imported from Prometheus-compatible sources within the specified namespace.",
"tags": [
"convert_prometheus"
]
},
"get": {
"operationId": "RouteConvertPrometheusCortexGetNamespace",
"parameters": [
{
"in": "path",
"name": "NamespaceTitle",
"required": true,
"type": "string"
}
],
"produces": [
"application/yaml"
],
"responses": {
"200": {
"description": "PrometheusNamespace",
"schema": {
"$ref": "#/definitions/PrometheusNamespace"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
},
"404": {
"description": "NotFound",
"schema": {
"$ref": "#/definitions/NotFound"
}
}
},
"summary": "Gets Grafana-managed alert rules that were imported from Prometheus-compatible sources for a specified namespace (folder).",
"tags": [
"convert_prometheus"
]
},
"post": {
"consumes": [
"application/yaml"
],
"description": "If the group already exists and was not imported from a Prometheus-compatible source initially,\nit will not be replaced and an error will be returned.",
"operationId": "RouteConvertPrometheusCortexPostRuleGroup",
"parameters": [
{
"in": "path",
"name": "NamespaceTitle",
"required": true,
"type": "string"
},
{
"in": "header",
"name": "x-grafana-alerting-datasource-uid",
"type": "string"
},
{
"in": "header",
"name": "x-grafana-alerting-recording-rules-paused",
"type": "boolean"
},
{
"in": "header",
"name": "x-grafana-alerting-alert-rules-paused",
"type": "boolean"
},
{
"in": "body",
"name": "Body",
"schema": {
"$ref": "#/definitions/PrometheusRuleGroup"
}
}
],
"produces": [
"application/json"
],
"responses": {
"202": {
"description": "ConvertPrometheusResponse",
"schema": {
"$ref": "#/definitions/ConvertPrometheusResponse"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
}
},
"summary": "Converts a Prometheus rule group into a Grafana rule group and creates or updates it within the specified namespace.",
"tags": [
"convert_prometheus"
],
"x-raw-request": "true"
}
},
"/convert/api/prom/rules/{NamespaceTitle}/{Group}": {
"delete": {
"operationId": "RouteConvertPrometheusCortexDeleteRuleGroup",
"parameters": [
{
"in": "path",
"name": "NamespaceTitle",
"required": true,
"type": "string"
},
{
"in": "path",
"name": "Group",
"required": true,
"type": "string"
}
],
"produces": [
"application/json"
],
"responses": {
"202": {
"description": "ConvertPrometheusResponse",
"schema": {
"$ref": "#/definitions/ConvertPrometheusResponse"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
}
},
"summary": "Deletes a specific rule group if it was imported from a Prometheus-compatible source.",
"tags": [
"convert_prometheus"
]
},
"get": {
"operationId": "RouteConvertPrometheusCortexGetRuleGroup",
"parameters": [
{
"in": "path",
"name": "NamespaceTitle",
"required": true,
"type": "string"
},
{
"in": "path",
"name": "Group",
"required": true,
"type": "string"
}
],
"produces": [
"application/yaml"
],
"responses": {
"200": {
"description": "PrometheusRuleGroup",
"schema": {
"$ref": "#/definitions/PrometheusRuleGroup"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
},
"404": {
"description": "NotFound",
"schema": {
"$ref": "#/definitions/NotFound"
}
}
},
"summary": "Gets a single rule group in Prometheus-compatible format if it was imported from a Prometheus-compatible source.",
"tags": [
"convert_prometheus"
]
}
},
"/convert/prometheus/config/v1/rules": {
"get": {
"operationId": "RouteConvertPrometheusGetRules",
+251 -1
View File
@@ -1102,6 +1102,253 @@
}
}
},
"/convert/api/prom/rules": {
"get": {
"produces": [
"application/yaml"
],
"tags": [
"convert_prometheus"
],
"summary": "Gets all Grafana-managed alert rules that were imported from Prometheus-compatible sources, grouped by namespace.",
"operationId": "RouteConvertPrometheusCortexGetRules",
"responses": {
"200": {
"description": "PrometheusNamespace",
"schema": {
"$ref": "#/definitions/PrometheusNamespace"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
},
"404": {
"description": "NotFound",
"schema": {
"$ref": "#/definitions/NotFound"
}
}
}
}
},
"/convert/api/prom/rules/{NamespaceTitle}": {
"get": {
"produces": [
"application/yaml"
],
"tags": [
"convert_prometheus"
],
"summary": "Gets Grafana-managed alert rules that were imported from Prometheus-compatible sources for a specified namespace (folder).",
"operationId": "RouteConvertPrometheusCortexGetNamespace",
"parameters": [
{
"type": "string",
"name": "NamespaceTitle",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "PrometheusNamespace",
"schema": {
"$ref": "#/definitions/PrometheusNamespace"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
},
"404": {
"description": "NotFound",
"schema": {
"$ref": "#/definitions/NotFound"
}
}
}
},
"post": {
"description": "If the group already exists and was not imported from a Prometheus-compatible source initially,\nit will not be replaced and an error will be returned.",
"consumes": [
"application/yaml"
],
"produces": [
"application/json"
],
"tags": [
"convert_prometheus"
],
"summary": "Converts a Prometheus rule group into a Grafana rule group and creates or updates it within the specified namespace.",
"operationId": "RouteConvertPrometheusCortexPostRuleGroup",
"parameters": [
{
"type": "string",
"name": "NamespaceTitle",
"in": "path",
"required": true
},
{
"type": "string",
"name": "x-grafana-alerting-datasource-uid",
"in": "header"
},
{
"type": "boolean",
"name": "x-grafana-alerting-recording-rules-paused",
"in": "header"
},
{
"type": "boolean",
"name": "x-grafana-alerting-alert-rules-paused",
"in": "header"
},
{
"name": "Body",
"in": "body",
"schema": {
"$ref": "#/definitions/PrometheusRuleGroup"
}
}
],
"responses": {
"202": {
"description": "ConvertPrometheusResponse",
"schema": {
"$ref": "#/definitions/ConvertPrometheusResponse"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
}
},
"x-raw-request": "true"
},
"delete": {
"produces": [
"application/json"
],
"tags": [
"convert_prometheus"
],
"summary": "Deletes all rule groups that were imported from Prometheus-compatible sources within the specified namespace.",
"operationId": "RouteConvertPrometheusCortexDeleteNamespace",
"parameters": [
{
"type": "string",
"name": "NamespaceTitle",
"in": "path",
"required": true
}
],
"responses": {
"202": {
"description": "ConvertPrometheusResponse",
"schema": {
"$ref": "#/definitions/ConvertPrometheusResponse"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
}
}
}
},
"/convert/api/prom/rules/{NamespaceTitle}/{Group}": {
"get": {
"produces": [
"application/yaml"
],
"tags": [
"convert_prometheus"
],
"summary": "Gets a single rule group in Prometheus-compatible format if it was imported from a Prometheus-compatible source.",
"operationId": "RouteConvertPrometheusCortexGetRuleGroup",
"parameters": [
{
"type": "string",
"name": "NamespaceTitle",
"in": "path",
"required": true
},
{
"type": "string",
"name": "Group",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "PrometheusRuleGroup",
"schema": {
"$ref": "#/definitions/PrometheusRuleGroup"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
},
"404": {
"description": "NotFound",
"schema": {
"$ref": "#/definitions/NotFound"
}
}
}
},
"delete": {
"produces": [
"application/json"
],
"tags": [
"convert_prometheus"
],
"summary": "Deletes a specific rule group if it was imported from a Prometheus-compatible source.",
"operationId": "RouteConvertPrometheusCortexDeleteRuleGroup",
"parameters": [
{
"type": "string",
"name": "NamespaceTitle",
"in": "path",
"required": true
},
{
"type": "string",
"name": "Group",
"in": "path",
"required": true
}
],
"responses": {
"202": {
"description": "ConvertPrometheusResponse",
"schema": {
"$ref": "#/definitions/ConvertPrometheusResponse"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
}
}
}
},
"/convert/prometheus/config/v1/rules": {
"get": {
"produces": [
@@ -8434,8 +8681,9 @@
}
},
"URL": {
"description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nThe Host field contains the host and port subcomponents of the URL.\nWhen the port is present, it is separated from the host with a colon.\nWhen the host is an IPv6 address, it must be enclosed in square brackets:\n\"[fe80::1]:80\". The [net.JoinHostPort] function combines a host and port\ninto a string suitable for the Host field, adding square brackets to\nthe host when necessary.\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use the [URL.EscapedPath] method, which preserves\nthe original encoding of Path.\n\nThe RawPath field is an optional field which is only set when the default\nencoding of Path is different from the escaped path. See the EscapedPath method\nfor more details.\n\nURL's String method uses the EscapedPath method to obtain the path.",
"type": "object",
"title": "URL is a custom URL type that allows validation at configuration load time.",
"title": "A URL represents a parsed URL (technically, a URI reference).",
"properties": {
"ForceQuery": {
"type": "boolean"
@@ -8711,6 +8959,7 @@
}
},
"alertGroups": {
"description": "AlertGroups alert groups",
"type": "array",
"items": {
"type": "object",
@@ -8997,6 +9246,7 @@
}
},
"gettableSilences": {
"description": "GettableSilences gettable silences",
"type": "array",
"items": {
"type": "object",
+1 -1
View File
@@ -501,7 +501,7 @@ func queryDataResponseToExecutionResults(c models.Condition, execResp *backend.Q
if frame.Fields[0].Len() == 1 {
v = frame.At(0, 0).(*float64) // type checked above
}
captureFn(frame.RefID, frame.Fields[0].Labels, v)
captureFn(refID, frame.Fields[0].Labels, v)
}
if refID == c.Condition {
+296 -217
View File
@@ -739,243 +739,322 @@ func TestEvaluate(t *testing.T) {
resp backend.QueryDataResponse
expected Results
error string
}{{
name: "is no data with no frames",
cond: models.Condition{
Data: []models.AlertQuery{{
RefID: "A",
DatasourceUID: "test",
}, {
RefID: "B",
DatasourceUID: expr.DatasourceUID,
}, {
RefID: "C",
DatasourceUID: expr.OldDatasourceUID,
}, {
RefID: "D",
DatasourceUID: expr.MLDatasourceUID,
}{
{
name: "is no data with no frames",
cond: models.Condition{
Data: []models.AlertQuery{{
RefID: "A",
DatasourceUID: "test",
}, {
RefID: "B",
DatasourceUID: expr.DatasourceUID,
}, {
RefID: "C",
DatasourceUID: expr.OldDatasourceUID,
}, {
RefID: "D",
DatasourceUID: expr.MLDatasourceUID,
}},
},
resp: backend.QueryDataResponse{
Responses: backend.Responses{
"A": {Frames: nil},
"B": {Frames: []*data.Frame{{Fields: nil}}},
"C": {Frames: nil},
"D": {Frames: []*data.Frame{{Fields: nil}}},
},
},
expected: Results{{
State: NoData,
Instance: data.Labels{
"datasource_uid": "test",
"ref_id": "A",
},
}},
},
resp: backend.QueryDataResponse{
Responses: backend.Responses{
"A": {Frames: nil},
"B": {Frames: []*data.Frame{{Fields: nil}}},
"C": {Frames: nil},
"D": {Frames: []*data.Frame{{Fields: nil}}},
{
name: "is no data for one frame with no fields",
cond: models.Condition{
Data: []models.AlertQuery{{
RefID: "A",
DatasourceUID: "test",
}},
},
},
expected: Results{{
State: NoData,
Instance: data.Labels{
"datasource_uid": "test",
"ref_id": "A",
resp: backend.QueryDataResponse{
Responses: backend.Responses{
"A": {Frames: []*data.Frame{{Fields: nil}}},
},
},
}},
}, {
name: "is no data for one frame with no fields",
cond: models.Condition{
Data: []models.AlertQuery{{
RefID: "A",
DatasourceUID: "test",
expected: Results{{
State: NoData,
Instance: data.Labels{
"datasource_uid": "test",
"ref_id": "A",
},
}},
},
resp: backend.QueryDataResponse{
Responses: backend.Responses{
"A": {Frames: []*data.Frame{{Fields: nil}}},
{
name: "results contains captured values for exact label matches",
cond: models.Condition{
Condition: "B",
},
resp: backend.QueryDataResponse{
Responses: backend.Responses{
"A": {
Frames: []*data.Frame{{
RefID: "A",
Fields: []*data.Field{
data.NewField(
"Value",
data.Labels{"foo": "bar"},
[]*float64{util.Pointer(10.0)},
),
},
}},
},
"B": {
Frames: []*data.Frame{{
RefID: "B",
Fields: []*data.Field{
data.NewField(
"Value",
data.Labels{"foo": "bar"},
[]*float64{util.Pointer(1.0)},
),
},
}},
},
},
},
expected: Results{{
State: Alerting,
Instance: data.Labels{
"foo": "bar",
},
Values: map[string]NumberValueCapture{
"A": {
Var: "A",
Labels: data.Labels{"foo": "bar"},
Value: util.Pointer(10.0),
},
"B": {
Var: "B",
Labels: data.Labels{"foo": "bar"},
Value: util.Pointer(1.0),
},
},
EvaluationString: "[ var='A' labels={foo=bar} value=10 ], [ var='B' labels={foo=bar} value=1 ]",
}},
},
expected: Results{{
State: NoData,
Instance: data.Labels{
"datasource_uid": "test",
"ref_id": "A",
{
name: "results contains captured values for subset of labels",
cond: models.Condition{
Condition: "B",
},
}},
}, {
name: "results contains captured values for exact label matches",
cond: models.Condition{
Condition: "B",
resp: backend.QueryDataResponse{
Responses: backend.Responses{
"A": {
Frames: []*data.Frame{{
RefID: "A",
Fields: []*data.Field{
data.NewField(
"Value",
data.Labels{"foo": "bar"},
[]*float64{util.Pointer(10.0)},
),
},
}},
},
"B": {
Frames: []*data.Frame{{
RefID: "B",
Fields: []*data.Field{
data.NewField(
"Value",
data.Labels{"foo": "bar", "bar": "baz"},
[]*float64{util.Pointer(1.0)},
),
},
}},
},
},
},
expected: Results{{
State: Alerting,
Instance: data.Labels{
"foo": "bar",
"bar": "baz",
},
Values: map[string]NumberValueCapture{
"A": {
Var: "A",
Labels: data.Labels{"foo": "bar"},
Value: util.Pointer(10.0),
},
"B": {
Var: "B",
Labels: data.Labels{"foo": "bar", "bar": "baz"},
Value: util.Pointer(1.0),
},
},
EvaluationString: "[ var='A' labels={foo=bar} value=10 ], [ var='B' labels={bar=baz, foo=bar} value=1 ]",
}},
},
resp: backend.QueryDataResponse{
Responses: backend.Responses{
"A": {
Frames: []*data.Frame{{
RefID: "A",
Fields: []*data.Field{
data.NewField(
"Value",
data.Labels{"foo": "bar"},
[]*float64{util.Pointer(10.0)},
),
},
}},
},
"B": {
Frames: []*data.Frame{{
RefID: "B",
Fields: []*data.Field{
data.NewField(
"Value",
data.Labels{"foo": "bar"},
[]*float64{util.Pointer(1.0)},
),
},
}},
{
name: "results contains error if condition frame has error",
cond: models.Condition{
Condition: "B",
},
resp: backend.QueryDataResponse{
Responses: backend.Responses{
"A": {
Frames: []*data.Frame{{
RefID: "A",
Fields: []*data.Field{
data.NewField(
"Value",
data.Labels{"foo": "bar"},
[]*float64{util.Pointer(10.0)},
),
},
}},
},
"B": {
Frames: []*data.Frame{{
RefID: "B",
Fields: []*data.Field{
data.NewField(
"Value",
data.Labels{"foo": "bar", "bar": "baz"},
[]*float64{util.Pointer(1.0)},
),
},
}},
Error: errors.New("some frame error"),
},
},
},
expected: Results{{
State: Error,
Error: errors.New("some frame error"),
EvaluationString: "",
}},
},
expected: Results{{
State: Alerting,
Instance: data.Labels{
"foo": "bar",
{
name: "results contain underlying error if condition frame has error that depends on another node",
cond: models.Condition{
Condition: "B",
},
Values: map[string]NumberValueCapture{
"A": {
Var: "A",
Labels: data.Labels{"foo": "bar"},
Value: util.Pointer(10.0),
},
"B": {
Var: "B",
Labels: data.Labels{"foo": "bar"},
Value: util.Pointer(1.0),
resp: backend.QueryDataResponse{
Responses: backend.Responses{
"A": {
Frames: []*data.Frame{{
RefID: "A",
Fields: []*data.Field{
data.NewField(
"Value",
data.Labels{"foo": "bar"},
[]*float64{util.Pointer(10.0)},
),
},
}},
Error: errors.New("another error depends on me"),
},
"B": {
Frames: []*data.Frame{{
RefID: "B",
Fields: []*data.Field{
data.NewField(
"Value",
data.Labels{"foo": "bar", "bar": "baz"},
[]*float64{util.Pointer(1.0)},
),
},
}},
Error: expr.MakeDependencyError("B", "A"),
},
},
},
EvaluationString: "[ var='A' labels={foo=bar} value=10 ], [ var='B' labels={foo=bar} value=1 ]",
}},
}, {
name: "results contains captured values for subset of labels",
cond: models.Condition{
Condition: "B",
expected: Results{{
State: Error,
Error: errors.New("another error depends on me"),
EvaluationString: "",
}},
},
resp: backend.QueryDataResponse{
Responses: backend.Responses{
"A": {
Frames: []*data.Frame{{
RefID: "A",
Fields: []*data.Field{
data.NewField(
"Value",
data.Labels{"foo": "bar"},
[]*float64{util.Pointer(10.0)},
),
},
}},
},
"B": {
Frames: []*data.Frame{{
RefID: "B",
Fields: []*data.Field{
data.NewField(
"Value",
data.Labels{"foo": "bar", "bar": "baz"},
[]*float64{util.Pointer(1.0)},
),
},
}},
{
name: "result values for all refIDs when a math no-op expression is used and two results share the same frame pointer",
cond: models.Condition{
Condition: "C",
},
resp: backend.QueryDataResponse{
Responses: backend.Responses{
"A": {
Frames: []*data.Frame{{
RefID: "A",
Fields: []*data.Field{
data.NewField(
"Value",
data.Labels{"foo": "bar"},
[]*float64{util.Pointer(10.0)},
),
},
}},
},
"B": {
// in a math no-op expression the frame for B is the same as A
// e.g. A = some_query, B = `${A}`
Frames: []*data.Frame{{
RefID: "A",
Fields: []*data.Field{
data.NewField(
"Value",
data.Labels{"foo": "bar"},
[]*float64{util.Pointer(10.0)},
),
},
}},
},
"C": {
Frames: []*data.Frame{{
RefID: "C",
Fields: []*data.Field{
data.NewField(
"Value",
data.Labels{"foo": "bar"},
[]*float64{util.Pointer(1.0)},
),
},
}},
},
},
},
expected: Results{{
State: Alerting,
Instance: data.Labels{
"foo": "bar",
},
Values: map[string]NumberValueCapture{
"A": {
Var: "A",
Labels: data.Labels{"foo": "bar"},
Value: util.Pointer(10.0),
},
"B": {
Var: "B",
Labels: data.Labels{"foo": "bar"},
Value: util.Pointer(10.0),
},
"C": {
Var: "C",
Labels: data.Labels{"foo": "bar"},
Value: util.Pointer(1.0),
},
},
EvaluationString: "[ var='A' labels={foo=bar} value=10 ], [ var='B' labels={foo=bar} value=10 ], [ var='C' labels={foo=bar} value=1 ]",
}},
},
expected: Results{{
State: Alerting,
Instance: data.Labels{
"foo": "bar",
"bar": "baz",
},
Values: map[string]NumberValueCapture{
"A": {
Var: "A",
Labels: data.Labels{"foo": "bar"},
Value: util.Pointer(10.0),
},
"B": {
Var: "B",
Labels: data.Labels{"foo": "bar", "bar": "baz"},
Value: util.Pointer(1.0),
},
},
EvaluationString: "[ var='A' labels={foo=bar} value=10 ], [ var='B' labels={bar=baz, foo=bar} value=1 ]",
}},
}, {
name: "results contains error if condition frame has error",
cond: models.Condition{
Condition: "B",
},
resp: backend.QueryDataResponse{
Responses: backend.Responses{
"A": {
Frames: []*data.Frame{{
RefID: "A",
Fields: []*data.Field{
data.NewField(
"Value",
data.Labels{"foo": "bar"},
[]*float64{util.Pointer(10.0)},
),
},
}},
},
"B": {
Frames: []*data.Frame{{
RefID: "B",
Fields: []*data.Field{
data.NewField(
"Value",
data.Labels{"foo": "bar", "bar": "baz"},
[]*float64{util.Pointer(1.0)},
),
},
}},
Error: errors.New("some frame error"),
},
},
},
expected: Results{{
State: Error,
Error: errors.New("some frame error"),
EvaluationString: "",
}},
}, {
name: "results contain underlying error if condition frame has error that depends on another node",
cond: models.Condition{
Condition: "B",
},
resp: backend.QueryDataResponse{
Responses: backend.Responses{
"A": {
Frames: []*data.Frame{{
RefID: "A",
Fields: []*data.Field{
data.NewField(
"Value",
data.Labels{"foo": "bar"},
[]*float64{util.Pointer(10.0)},
),
},
}},
Error: errors.New("another error depends on me"),
},
"B": {
Frames: []*data.Frame{{
RefID: "B",
Fields: []*data.Field{
data.NewField(
"Value",
data.Labels{"foo": "bar", "bar": "baz"},
[]*float64{util.Pointer(1.0)},
),
},
}},
Error: expr.MakeDependencyError("B", "A"),
},
},
},
expected: Results{{
State: Error,
Error: errors.New("another error depends on me"),
EvaluationString: "",
}},
}}
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
@@ -6,6 +6,7 @@ import (
"fmt"
"time"
"github.com/grafana/grafana/pkg/apimachinery/errutil"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/dashboards"
@@ -29,6 +30,11 @@ type ruleAccessControlService interface {
CanWriteAllRules(ctx context.Context, user identity.Requester) (bool, error)
}
var errProvenanceMismatch = errutil.NewBase(errutil.StatusConflict, "alerting.provenanceMismatch").MustTemplate(
"cannot {{ .Public.Operation }} with provided provenance '{{ .Public.ProvidedProvenance }}', needs '{{ .Public.StoredProvenance }}'",
errutil.WithPublic("cannot {{ .Public.Operation }} with provided provenance '{{ .Public.ProvidedProvenance }}', needs '{{ .Public.StoredProvenance }}'"),
)
type NotificationSettingsValidatorProvider interface {
Validator(ctx context.Context, orgID int64) (notifier.NotificationSettingsValidator, error)
}
@@ -445,27 +451,42 @@ func (service *AlertRuleService) ReplaceRuleGroup(ctx context.Context, user iden
}
func (service *AlertRuleService) DeleteRuleGroup(ctx context.Context, user identity.Requester, namespaceUID, group string, provenance models.Provenance) error {
delta, err := store.CalculateRuleGroupDelete(ctx, service.ruleStore, models.AlertRuleGroupKey{
OrgID: user.GetOrgID(),
NamespaceUID: namespaceUID,
RuleGroup: group,
return service.DeleteRuleGroups(ctx, user, provenance, &FilterOptions{
NamespaceUIDs: []string{namespaceUID},
RuleGroups: []string{group},
})
}
// DeleteRuleGroups deletes alert rule groups by the specified filter options.
func (service *AlertRuleService) DeleteRuleGroups(ctx context.Context, user identity.Requester, provenance models.Provenance, filterOpts *FilterOptions) error {
q := models.ListAlertRulesQuery{}
q = filterOpts.apply(q)
q.OrgID = user.GetOrgID()
deltas, err := store.CalculateRuleGroupsDelete(ctx, service.ruleStore, user.GetOrgID(), &q)
if err != nil {
return err
}
// check if the current user has permissions to all rules and can bypass the regular authorization validation.
can, err := service.authz.CanWriteAllRules(ctx, user)
if err != nil {
return err
}
if !can {
if err := service.authz.AuthorizeRuleGroupWrite(ctx, user, delta); err != nil {
return err
// Perform all deletions in a transaction
return service.xact.InTransaction(ctx, func(ctx context.Context) error {
for _, delta := range deltas {
can, err := service.authz.CanWriteAllRules(ctx, user)
if err != nil {
return err
}
if !can {
if err := service.authz.AuthorizeRuleGroupWrite(ctx, user, delta); err != nil {
return err
}
}
err = service.persistDelta(ctx, user, delta, provenance)
if err != nil {
return err
}
}
}
return service.persistDelta(ctx, user, delta, provenance)
return nil
})
}
func (service *AlertRuleService) calcDelta(ctx context.Context, user identity.Requester, group models.AlertRuleGroup) (*store.GroupDelta, error) {
@@ -526,7 +547,13 @@ func (service *AlertRuleService) persistDelta(ctx context.Context, user identity
return err
}
if canUpdate := validation.CanUpdateProvenanceInRuleGroup(storedProvenance, provenance); !canUpdate {
return fmt.Errorf("cannot delete with provided provenance '%s', needs '%s'", provenance, storedProvenance)
return errProvenanceMismatch.Build(errutil.TemplateData{
Public: map[string]interface{}{
"ProvidedProvenance": provenance,
"StoredProvenance": storedProvenance,
"Operation": "delete",
},
})
}
}
if err := service.deleteRules(ctx, user.GetOrgID(), delta.Delete...); err != nil {
@@ -543,7 +570,13 @@ func (service *AlertRuleService) persistDelta(ctx context.Context, user identity
return err
}
if canUpdate := validation.CanUpdateProvenanceInRuleGroup(storedProvenance, provenance); !canUpdate {
return fmt.Errorf("cannot update with provided provenance '%s', needs '%s'", provenance, storedProvenance)
return errProvenanceMismatch.Build(errutil.TemplateData{
Public: map[string]interface{}{
"ProvidedProvenance": provenance,
"StoredProvenance": storedProvenance,
"Operation": "update",
},
})
}
updates = append(updates, models.UpdateRule{
Existing: update.Existing,
@@ -689,7 +722,13 @@ func (service *AlertRuleService) DeleteAlertRule(ctx context.Context, user ident
return err
}
if storedProvenance != provenance && storedProvenance != models.ProvenanceNone {
return fmt.Errorf("cannot delete with provided provenance '%s', needs '%s'", provenance, storedProvenance)
return errProvenanceMismatch.Build(errutil.TemplateData{
Public: map[string]interface{}{
"ProvidedProvenance": provenance,
"StoredProvenance": storedProvenance,
"Operation": "delete",
},
})
}
can, err := service.authz.CanWriteAllRules(ctx, user)
@@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"math/rand"
"slices"
"strconv"
"strings"
"testing"
@@ -1577,7 +1578,7 @@ func TestDeleteRuleGroup(t *testing.T) {
assert.Equal(t, u, user)
assert.Equal(t, groupKey, change.GroupKey)
assert.Contains(t, change.AffectedGroups, groupKey)
assert.EqualValues(t, rules, change.AffectedGroups[groupKey])
assert.ElementsMatch(t, rules, change.AffectedGroups[groupKey])
assert.Empty(t, change.Update)
assert.Empty(t, change.New)
assert.Len(t, change.Delete, len(rules))
@@ -1597,6 +1598,228 @@ func TestDeleteRuleGroup(t *testing.T) {
})
}
func TestDeleteRuleGroups(t *testing.T) {
orgID1 := rand.Int63()
orgID2 := rand.Int63()
u := &user.SignedInUser{OrgID: orgID1}
// Create groups across different orgs and namespaces
groupKey1 := models.AlertRuleGroupKey{
OrgID: orgID1,
NamespaceUID: "namespace1",
RuleGroup: "group1",
}
groupKey2 := models.AlertRuleGroupKey{
OrgID: orgID1,
NamespaceUID: "namespace2",
RuleGroup: "group2",
}
groupKey3 := models.AlertRuleGroupKey{
OrgID: orgID1,
NamespaceUID: "namespace3",
RuleGroup: "group3",
}
groupKey4 := models.AlertRuleGroupKey{
OrgID: orgID2, // Different org
NamespaceUID: "namespace1",
RuleGroup: "group1",
}
gen := models.RuleGen
// Create rules for each group
rules1 := gen.With(gen.WithGroupKey(groupKey1)).GenerateManyRef(2)
rules2 := gen.With(gen.WithGroupKey(groupKey2)).GenerateManyRef(3)
rules3 := gen.With(gen.WithGroupKey(groupKey3)).GenerateManyRef(2)
rules4 := gen.With(gen.WithGroupKey(groupKey4)).GenerateManyRef(2)
org1Rules := slices.Concat(rules1, rules2, rules3)
org2Rules := rules4
initServiceWithData := func(t *testing.T) (*AlertRuleService, *fakes.RuleStore, *fakes.FakeProvisioningStore, *fakeRuleAccessControlService) {
service, ruleStore, provenanceStore, ac := initService(t)
ruleStore.Rules = map[int64][]*models.AlertRule{
orgID1: org1Rules,
orgID2: org2Rules,
}
// Set provenance for all rules
for _, rules := range []([]*models.AlertRule){org1Rules, org2Rules} {
for _, rule := range rules {
err := provenanceStore.SetProvenance(context.Background(), rule, rule.OrgID, models.ProvenanceAPI)
require.NoError(t, err)
}
}
return service, ruleStore, provenanceStore, ac
}
getUIDs := func(rules []*models.AlertRule) []string {
uids := make([]string, 0, len(rules))
for _, rule := range rules {
uids = append(uids, rule.UID)
}
return uids
}
t.Run("when deleting specific groups", func(t *testing.T) {
filterOpts := &FilterOptions{
NamespaceUIDs: []string{"namespace1"},
RuleGroups: []string{"group1"},
}
t.Run("when user can write all rules", func(t *testing.T) {
service, ruleStore, _, ac := initServiceWithData(t)
ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
return true, nil
}
err := service.DeleteRuleGroups(context.Background(), u, models.ProvenanceAPI, filterOpts)
require.NoError(t, err)
require.Len(t, ac.Calls, 1)
assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method)
// Verify only rules from group1 in org1 were deleted
deletes := getDeletedRules(t, ruleStore)
require.Len(t, deletes, 1)
require.ElementsMatch(t, getUIDs(rules1), deletes[0].uids)
})
t.Run("when user cannot write all rules", func(t *testing.T) {
t.Run("should not delete if not authorized", func(t *testing.T) {
service, ruleStore, _, ac := initServiceWithData(t)
ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
return false, nil
}
expectedErr := errors.New("test error")
ac.AuthorizeRuleChangesFunc = func(ctx context.Context, user identity.Requester, change *store.GroupDelta) error {
return expectedErr
}
err := service.DeleteRuleGroups(context.Background(), u, models.ProvenanceAPI, filterOpts)
require.ErrorIs(t, err, expectedErr)
require.Len(t, ac.Calls, 2)
assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method)
assert.Equal(t, "AuthorizeRuleGroupWrite", ac.Calls[1].Method)
deletes := getDeletedRules(t, ruleStore)
require.Empty(t, deletes)
})
t.Run("should delete group1 when authorized", func(t *testing.T) {
service, ruleStore, _, ac := initServiceWithData(t)
ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
return false, nil
}
ac.AuthorizeRuleChangesFunc = func(ctx context.Context, user identity.Requester, change *store.GroupDelta) error {
assert.Equal(t, u, user)
assert.Equal(t, groupKey1, change.GroupKey)
assert.ElementsMatch(t, rules1, change.AffectedGroups[groupKey1])
assert.Empty(t, change.Update)
assert.Empty(t, change.New)
assert.Len(t, change.Delete, len(rules1))
return nil
}
err := service.DeleteRuleGroups(context.Background(), u, models.ProvenanceAPI, filterOpts)
require.NoError(t, err)
require.Len(t, ac.Calls, 2)
assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method)
assert.Equal(t, "AuthorizeRuleGroupWrite", ac.Calls[1].Method)
deletes := getDeletedRules(t, ruleStore)
require.Len(t, deletes, 1)
require.ElementsMatch(t, getUIDs(rules1), deletes[0].uids)
})
})
})
t.Run("when deleting multiple groups from multiple namespaces", func(t *testing.T) {
filterOpts := &FilterOptions{
NamespaceUIDs: []string{"namespace1", "namespace2"},
RuleGroups: []string{"group1", "group2"},
}
t.Run("should delete all matching groups from correct org", func(t *testing.T) {
service, ruleStore, _, ac := initServiceWithData(t)
ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
return true, nil
}
err := service.DeleteRuleGroups(context.Background(), u, models.ProvenanceAPI, filterOpts)
require.NoError(t, err)
deletes := getDeletedRules(t, ruleStore)
require.Len(t, deletes, 2)
require.ElementsMatch(
t,
slices.Concat(getUIDs(rules1), getUIDs(rules2)),
slices.Concat(deletes[0].uids, deletes[1].uids),
)
})
})
t.Run("when filtering by imported Prometheus rules", func(t *testing.T) {
filterOpts := &FilterOptions{
ImportedPrometheusRule: util.Pointer(true),
NamespaceUIDs: []string{"namespace1"},
}
t.Run("when the group is not imported", func(t *testing.T) {
filterOpts.RuleGroups = []string{groupKey1.RuleGroup}
service, _, _, ac := initServiceWithData(t)
ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
return true, nil
}
err := service.DeleteRuleGroups(context.Background(), u, models.ProvenanceAPI, filterOpts)
require.ErrorIs(t, err, models.ErrAlertRuleGroupNotFound)
})
t.Run("when the group is imported", func(t *testing.T) {
importedGroup := models.AlertRuleGroupKey{
OrgID: orgID1,
NamespaceUID: "namespace1",
RuleGroup: "newgroup",
}
importedRules := gen.With(
gen.WithGroupKey(importedGroup),
gen.WithPrometheusOriginalRuleDefinition("something"),
).GenerateManyRef(2)
filterOpts.RuleGroups = []string{importedGroup.RuleGroup}
service, ruleStore, _, ac := initServiceWithData(t)
ruleStore.Rules[orgID1] = append(ruleStore.Rules[orgID1], importedRules...)
ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
return true, nil
}
err := service.DeleteRuleGroups(context.Background(), u, models.ProvenanceAPI, filterOpts)
require.NoError(t, err)
deletes := getDeletedRules(t, ruleStore)
require.Len(t, deletes, 1)
require.ElementsMatch(t, getUIDs(importedRules), deletes[0].uids)
})
})
t.Run("with no matching rule groups", func(t *testing.T) {
filterOpts := &FilterOptions{
NamespaceUIDs: []string{"non-existent"},
RuleGroups: []string{"non-existent"},
}
service, ruleStore, _, ac := initServiceWithData(t)
ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
return true, nil
}
err := service.DeleteRuleGroups(context.Background(), u, models.ProvenanceAPI, filterOpts)
require.ErrorIs(t, err, models.ErrAlertRuleGroupNotFound)
deletes := getDeletedRules(t, ruleStore)
require.Empty(t, deletes)
})
}
func TestProvisiongWithFullpath(t *testing.T) {
tracer := tracing.InitializeTracerForTest()
inProcBus := bus.ProvideBus(tracer)
@@ -1696,6 +1919,31 @@ func getDeleteQueries(ruleStore *fakes.RuleStore) []fakes.GenericRecordedQuery {
return result
}
type deleteRuleOperation struct {
orgID int64
uids []string
}
func getDeletedRules(t *testing.T, ruleStore *fakes.RuleStore) []deleteRuleOperation {
t.Helper()
queries := getDeleteQueries(ruleStore)
operations := make([]deleteRuleOperation, 0, len(queries))
for _, q := range queries {
orgID, ok := q.Params[0].(int64)
require.True(t, ok, "orgID parameter should be int64")
uids, ok := q.Params[1].([]string)
require.True(t, ok, "uids parameter should be []string")
operations = append(operations, deleteRuleOperation{
orgID: orgID,
uids: uids,
})
}
return operations
}
func createAlertRuleService(t *testing.T, folderService folder.Service) AlertRuleService {
t.Helper()
sqlStore := db.InitTestDB(t)
+39 -15
View File
@@ -221,15 +221,13 @@ func CalculateRuleUpdate(ctx context.Context, ruleReader RuleReader, rule *model
return calculateChanges(ctx, ruleReader, rule.GetGroupKey(), existingGroupRules, newGroup)
}
// CalculateRuleGroupDelete calculates GroupDelta that reflects an operation of removing entire group
func CalculateRuleGroupDelete(ctx context.Context, ruleReader RuleReader, groupKey models.AlertRuleGroupKey) (*GroupDelta, error) {
// List all rules in the group.
q := models.ListAlertRulesQuery{
OrgID: groupKey.OrgID,
NamespaceUIDs: []string{groupKey.NamespaceUID},
RuleGroups: []string{groupKey.RuleGroup},
// CalculateRuleGroupsDelete calculates []*GroupDelta that reflects an operation of removing multiple groups
func CalculateRuleGroupsDelete(ctx context.Context, ruleReader RuleReader, orgID int64, query *models.ListAlertRulesQuery) ([]*GroupDelta, error) {
if query == nil {
query = &models.ListAlertRulesQuery{}
}
ruleList, err := ruleReader.ListAlertRules(ctx, &q)
query.OrgID = orgID
ruleList, err := ruleReader.ListAlertRules(ctx, query)
if err != nil {
return nil, err
}
@@ -237,14 +235,40 @@ func CalculateRuleGroupDelete(ctx context.Context, ruleReader RuleReader, groupK
return nil, models.ErrAlertRuleGroupNotFound.Errorf("")
}
delta := &GroupDelta{
GroupKey: groupKey,
Delete: ruleList,
AffectedGroups: map[models.AlertRuleGroupKey]models.RulesGroup{
groupKey: ruleList,
},
groups := models.GroupByAlertRuleGroupKey(ruleList)
deltas := make([]*GroupDelta, 0, len(groups))
for groupKey := range groups {
delta := &GroupDelta{
GroupKey: groupKey,
Delete: groups[groupKey],
AffectedGroups: map[models.AlertRuleGroupKey]models.RulesGroup{
groupKey: groups[groupKey],
},
}
if err != nil {
return nil, err
}
deltas = append(deltas, delta)
}
return delta, nil
return deltas, nil
}
// CalculateRuleGroupDelete calculates GroupDelta that reflects an operation of removing entire group
func CalculateRuleGroupDelete(ctx context.Context, ruleReader RuleReader, groupKey models.AlertRuleGroupKey) (*GroupDelta, error) {
q := &models.ListAlertRulesQuery{
NamespaceUIDs: []string{groupKey.NamespaceUID},
RuleGroups: []string{groupKey.RuleGroup},
}
deltas, err := CalculateRuleGroupsDelete(ctx, ruleReader, groupKey.OrgID, q)
if err != nil {
return nil, err
}
if len(deltas) != 1 {
return nil, fmt.Errorf("expected to get a single group delta, got %d", len(deltas))
}
return deltas[0], nil
}
// CalculateRuleDelete calculates GroupDelta that reflects an operation of removing a rule from the group.
+87 -2
View File
@@ -428,6 +428,91 @@ func TestCalculateAutomaticChanges(t *testing.T) {
})
}
func TestCalculateRuleGroupsDelete(t *testing.T) {
orgId := int64(rand.Int31())
gen := models.RuleGen
t.Run("returns ErrAlertRuleGroupNotFound when namespace has no rules", func(t *testing.T) {
fakeStore := fakes.NewRuleStore(t)
otherRules := gen.With(gen.WithOrgID(orgId), gen.WithNamespaceUID("ns-1")).GenerateManyRef(3)
fakeStore.Rules[orgId] = otherRules
query := &models.ListAlertRulesQuery{
NamespaceUIDs: []string{"ns-2"},
}
deltas, err := CalculateRuleGroupsDelete(context.Background(), fakeStore, orgId, query)
require.ErrorIs(t, err, models.ErrAlertRuleGroupNotFound)
require.Nil(t, deltas)
})
t.Run("returns deltas for all affected groups in namespace", func(t *testing.T) {
fakeStore := fakes.NewRuleStore(t)
folder := randFolder()
// Create rules in two groups in target namespace
group1Key := models.AlertRuleGroupKey{
OrgID: orgId,
NamespaceUID: folder.UID,
RuleGroup: util.GenerateShortUID(),
}
group2Key := models.AlertRuleGroupKey{
OrgID: orgId,
NamespaceUID: folder.UID,
RuleGroup: util.GenerateShortUID(),
}
group1Rules := gen.With(gen.WithGroupKey(group1Key)).GenerateManyRef(3)
group2Rules := gen.With(gen.WithGroupKey(group2Key)).GenerateManyRef(2)
allNamespaceRules := append(group1Rules, group2Rules...)
// Create rules in different namespace
otherRules := gen.With(gen.WithOrgID(orgId), gen.WithNamespaceUIDNotIn(folder.UID)).GenerateManyRef(3)
fakeStore.Rules[orgId] = append(allNamespaceRules, otherRules...)
query := &models.ListAlertRulesQuery{
NamespaceUIDs: []string{folder.UID},
}
deltas, err := CalculateRuleGroupsDelete(context.Background(), fakeStore, orgId, query)
require.NoError(t, err)
require.Len(t, deltas, 2, "expected deltas for two groups")
// Verify each group's delta
for _, delta := range deltas {
require.True(t, delta.GroupKey == group1Key || delta.GroupKey == group2Key)
require.Empty(t, delta.Update)
require.Empty(t, delta.New)
require.Contains(t, delta.AffectedGroups, delta.GroupKey)
if delta.GroupKey == group1Key {
require.ElementsMatch(t, group1Rules, delta.Delete)
require.ElementsMatch(t, group1Rules, delta.AffectedGroups[delta.GroupKey])
} else {
require.ElementsMatch(t, group2Rules, delta.Delete)
require.ElementsMatch(t, group2Rules, delta.AffectedGroups[delta.GroupKey])
}
}
})
t.Run("fails if store returns error", func(t *testing.T) {
fakeStore := fakes.NewRuleStore(t)
expectedErr := errors.New("store error")
fakeStore.Hook = func(cmd any) error {
switch cmd.(type) {
case models.ListAlertRulesQuery:
return expectedErr
}
return nil
}
deltas, err := CalculateRuleGroupsDelete(context.Background(), fakeStore, orgId, nil)
require.ErrorIs(t, err, expectedErr)
require.Nil(t, deltas)
})
}
func TestCalculateRuleGroupDelete(t *testing.T) {
gen := models.RuleGen
fakeStore := fakes.NewRuleStore(t)
@@ -449,13 +534,13 @@ func TestCalculateRuleGroupDelete(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, groupKey, delta.GroupKey)
assert.EqualValues(t, groupRules, delta.Delete)
assert.ElementsMatch(t, groupRules, delta.Delete)
assert.Empty(t, delta.Update)
assert.Empty(t, delta.New)
assert.Len(t, delta.AffectedGroups, 1)
assert.Equal(t, models.RulesGroup(groupRules), delta.AffectedGroups[delta.GroupKey])
assert.ElementsMatch(t, groupRules, delta.AffectedGroups[delta.GroupKey])
})
}
+18
View File
@@ -214,6 +214,15 @@ func (f *RuleStore) ListAlertRules(_ context.Context, q *models.ListAlertRulesQu
if len(q.RuleUIDs) > 0 && !slices.Contains(q.RuleUIDs, r.UID) {
continue
}
if q.ImportedPrometheusRule != nil {
hasOriginalRuleDefinition := r.PrometheusRuleDefinition() != ""
if *q.ImportedPrometheusRule && !hasOriginalRuleDefinition {
continue
}
if !*q.ImportedPrometheusRule && hasOriginalRuleDefinition {
continue
}
}
ruleList = append(ruleList, r)
}
@@ -308,12 +317,21 @@ func (f *RuleStore) InsertAlertRules(_ context.Context, _ *models.UserUID, q []m
defer f.mtx.Unlock()
f.RecordedOps = append(f.RecordedOps, q)
ids := make([]models.AlertRuleKeyWithId, 0, len(q))
rulesPerOrg := map[int64][]models.AlertRule{}
for _, rule := range q {
ids = append(ids, models.AlertRuleKeyWithId{
AlertRuleKey: rule.GetKey(),
ID: rand.Int63(),
})
rulesPerOrg[rule.OrgID] = append(rulesPerOrg[rule.OrgID], rule)
}
for orgID, rules := range rulesPerOrg {
for _, rule := range rules {
f.Rules[orgID] = append(f.Rules[orgID], &rule)
}
}
if err := f.Hook(q); err != nil {
return ids, err
}
+5
View File
@@ -14,7 +14,12 @@ func (cfg *Cfg) readAnonymousSettings() {
anonSettings := AnonymousSettings{}
anonSettings.Enabled = anonSection.Key("enabled").MustBool(false)
anonSettings.OrgName = valueAsString(anonSection, "org_name", "")
// Deprecated:
// only viewer role is supported
anonSettings.OrgRole = valueAsString(anonSection, "org_role", "")
if anonSettings.OrgRole != "Viewer" {
cfg.Logger.Warn("auth.anonymous.org_role is deprecated, only viewer role is supported")
}
anonSettings.HideVersion = anonSection.Key("hide_version").MustBool(false)
anonSettings.DeviceLimit = anonSection.Key("device_limit").MustInt64(0)
cfg.Anonymous = anonSettings
+1 -1
View File
@@ -26,7 +26,7 @@ type mockService struct {
}
// NewStorage implements Service.
func (m *mockService) NewStorage(gr schema.GroupResource, legacy rest.LegacyStorage, storage rest.Storage) (rest.Storage, error) {
func (m *mockService) NewStorage(gr schema.GroupResource, legacy rest.Storage, storage rest.Storage) (rest.Storage, error) {
return nil, fmt.Errorf("not implemented")
}
+2 -2
View File
@@ -15,7 +15,7 @@ import (
)
func (m *service) NewStorage(gr schema.GroupResource,
legacy grafanarest.LegacyStorage,
legacy grafanarest.Storage,
storage grafanarest.Storage,
) (grafanarest.Storage, error) {
status, err := m.Status(context.Background(), gr)
@@ -53,7 +53,7 @@ func (m *service) NewStorage(gr schema.GroupResource,
// When a resource is marked as "migrating", all write requests will be 503 unavailable
type runtimeDualWriter struct {
service Service
legacy grafanarest.LegacyStorage
legacy grafanarest.Storage
unified grafanarest.Storage
dualwrite grafanarest.Storage
gr schema.GroupResource
@@ -76,10 +76,10 @@ func TestManagedMode3_Create(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := (rest.LegacyStorage)(nil)
l := (rest.Storage)(nil)
s := (rest.Storage)(nil)
ls := legacyStoreMock{&mock.Mock{}, l}
ls := storageMock{&mock.Mock{}, l}
us := storageMock{&mock.Mock{}, s}
if tt.setupLegacyFn != nil {
@@ -148,10 +148,10 @@ func TestManagedMode3_Get(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := (rest.LegacyStorage)(nil)
l := (rest.Storage)(nil)
s := (rest.Storage)(nil)
ls := legacyStoreMock{&mock.Mock{}, l}
ls := storageMock{&mock.Mock{}, l}
us := storageMock{&mock.Mock{}, s}
if tt.setupLegacyFn != nil {
@@ -249,10 +249,10 @@ func TestManagedMode3_CreateWhileMigrating(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := (rest.LegacyStorage)(nil)
l := (rest.Storage)(nil)
s := (rest.Storage)(nil)
ls := legacyStoreMock{&mock.Mock{}, l}
ls := storageMock{&mock.Mock{}, l}
us := storageMock{&mock.Mock{}, s}
if tt.setupLegacyFn != nil {
+1 -1
View File
@@ -14,7 +14,7 @@ type staticService struct {
cfg *setting.Cfg
}
func (m *staticService) NewStorage(gr schema.GroupResource, legacy rest.LegacyStorage, storage rest.Storage) (rest.Storage, error) {
func (m *staticService) NewStorage(gr schema.GroupResource, legacy rest.Storage, storage rest.Storage) (rest.Storage, error) {
return nil, fmt.Errorf("not implemented")
}
@@ -13,102 +13,11 @@ import (
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
)
type legacyStoreMock struct {
*mock.Mock
grafanarest.LegacyStorage
}
type storageMock struct {
*mock.Mock
grafanarest.Storage
}
func (m legacyStoreMock) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
select {
case <-ctx.Done():
return nil, errors.New("context canceled")
default:
}
args := m.Called(ctx, name, options)
if err := args.Get(1); err != nil {
return nil, err.(error)
}
return args.Get(0).(runtime.Object), args.Error(1)
}
func (m legacyStoreMock) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
select {
case <-ctx.Done():
return nil, errors.New("context canceled")
default:
}
args := m.Called(ctx, obj, createValidation, options)
if err := args.Get(1); err != nil {
return nil, err.(error)
}
return args.Get(0).(runtime.Object), args.Error(1)
}
func (m legacyStoreMock) List(ctx context.Context, options *metainternalversion.ListOptions) (runtime.Object, error) {
select {
case <-ctx.Done():
return nil, errors.New("context canceled")
default:
}
args := m.Called(ctx, options)
if err := args.Get(1); err != nil {
return nil, err.(error)
}
return args.Get(0).(runtime.Object), args.Error(1)
}
func (m legacyStoreMock) NewList() runtime.Object {
return nil
}
func (m legacyStoreMock) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) {
select {
case <-ctx.Done():
return nil, false, errors.New("context canceled")
default:
}
args := m.Called(ctx, name, objInfo, createValidation, updateValidation, forceAllowCreate, options)
if err := args.Get(2); err != nil {
return nil, false, err.(error)
}
return args.Get(0).(runtime.Object), args.Bool(1), args.Error(2)
}
func (m legacyStoreMock) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
select {
case <-ctx.Done():
return nil, false, errors.New("context canceled")
default:
}
args := m.Called(ctx, name, deleteValidation, options)
if err := args.Get(2); err != nil {
return nil, false, err.(error)
}
return args.Get(0).(runtime.Object), args.Bool(1), args.Error(2)
}
func (m legacyStoreMock) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *metainternalversion.ListOptions) (runtime.Object, error) {
select {
case <-ctx.Done():
return nil, errors.New("context canceled")
default:
}
args := m.Called(ctx, deleteValidation, options, listOptions)
if err := args.Get(1); err != nil {
return nil, err.(error)
}
return args.Get(0).(runtime.Object), args.Error(1)
}
// Unified Store
func (m storageMock) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
select {
+1 -1
View File
@@ -36,7 +36,7 @@ type Service interface {
ShouldManage(gr schema.GroupResource) bool
// Create a managed k8s storage instance
NewStorage(gr schema.GroupResource, legacy grafanarest.LegacyStorage, storage grafanarest.Storage) (grafanarest.Storage, error)
NewStorage(gr schema.GroupResource, legacy grafanarest.Storage, storage grafanarest.Storage) (grafanarest.Storage, error)
// Check if the dual writes is reading from unified storage (mode3++)
ReadFromUnified(ctx context.Context, gr schema.GroupResource) (bool, error)
+4 -4
View File
@@ -134,7 +134,7 @@ require (
github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71 // indirect
github.com/dolthub/vitess v0.0.0-20250123002143-3b45b8cacbfa // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/elazarl/goproxy v1.7.0 // indirect
github.com/elazarl/goproxy v1.7.1 // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/envoyproxy/protoc-gen-validate v1.1.0 // indirect
@@ -183,7 +183,7 @@ require (
github.com/google/cel-go v0.22.1 // indirect
github.com/google/flatbuffers v24.3.25+incompatible // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/s2a-go v0.1.8 // indirect
@@ -199,7 +199,7 @@ require (
github.com/grafana/grafana-app-sdk/logging v0.30.0 // indirect
github.com/grafana/grafana-aws-sdk v0.31.5 // indirect
github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6 // indirect
github.com/grafana/grafana-plugin-sdk-go v0.266.0 // indirect
github.com/grafana/grafana-plugin-sdk-go v0.267.0 // indirect
github.com/grafana/grafana/pkg/aggregator v0.0.0-20250220163425-b4c4b9abbdc8 // indirect
github.com/grafana/grafana/pkg/promlib v0.0.8 // indirect
github.com/grafana/grafana/pkg/semconv v0.0.0-20250220164708-c8d4ff28a450 // indirect
@@ -382,7 +382,7 @@ require (
google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
google.golang.org/protobuf v1.36.4 // indirect
google.golang.org/protobuf v1.36.5 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect
+8 -7
View File
@@ -334,8 +334,8 @@ github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5m
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/elazarl/goproxy v1.7.0 h1:EXv2nV4EjM60ZtsEVLYJG4oBXhDGutMKperpHsZ/v+0=
github.com/elazarl/goproxy v1.7.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ=
github.com/elazarl/goproxy v1.7.1 h1:1P7LPSxbqtNxusFnXclj6O56pjfq1xOQZ6a0mwwKUlY=
github.com/elazarl/goproxy v1.7.1/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
@@ -515,8 +515,9 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
@@ -584,8 +585,8 @@ github.com/grafana/grafana-aws-sdk v0.31.5 h1:4HpMQx7n4Qqoi7Bgu8KHQ2QKT9fYYdHilX
github.com/grafana/grafana-aws-sdk v0.31.5/go.mod h1:5p4Cjyr5ZiR6/RT2nFWkJ8XpIKgX4lAUmUMu70m2yCM=
github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6 h1:OfCkitCuomzZKW1WYHrG8MxKwtMhALb7jqoj+487eTg=
github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6/go.mod h1:V7y2BmsWxS3A9Ohebwn4OiSfJJqi//4JQydQ8fHTduo=
github.com/grafana/grafana-plugin-sdk-go v0.266.0 h1:YP+iEpXH3HRX9Xo4NHjsrJhN2W7uVTtkLNzMHYbmiLI=
github.com/grafana/grafana-plugin-sdk-go v0.266.0/go.mod h1:bxkXrBQ4QSmOncsWdIOcpgP+M6wajQNMAPXlbWrqAWY=
github.com/grafana/grafana-plugin-sdk-go v0.267.0 h1:4AcuLEE0UeFG0Eo3P8r1FDS89hgZZ73K550DRz1fgjQ=
github.com/grafana/grafana-plugin-sdk-go v0.267.0/go.mod h1:OuwS4c/JYgn0rr/w5zhJBpLo4gKm/vw15RsfpYAvK9Q=
github.com/grafana/grafana/pkg/aggregator v0.0.0-20250220163425-b4c4b9abbdc8 h1:9qOLpC21AmXZqZ6rUhrBWl2mVqS3CzV53pzw0BCuHt0=
github.com/grafana/grafana/pkg/aggregator v0.0.0-20250220163425-b4c4b9abbdc8/go.mod h1:deLQ/ywLvpVGbncRGUA4UDGt8a5Ei9sivOP+x6AQ2ko=
github.com/grafana/grafana/pkg/promlib v0.0.8 h1:VUWsqttdf0wMI4j9OX9oNrykguQpZcruudDAFpJJVw0=
@@ -1552,8 +1553,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
+4 -4
View File
@@ -15,7 +15,7 @@ require (
github.com/grafana/authlib/types v0.0.0-20250224151205-5ef97131cc82
github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040
github.com/grafana/grafana v11.4.0-00010101000000-000000000000+incompatible
github.com/grafana/grafana-plugin-sdk-go v0.266.0
github.com/grafana/grafana-plugin-sdk-go v0.267.0
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250220154326-6e5de80ef295
github.com/grafana/grafana/pkg/apiserver v0.0.0-20250220154326-6e5de80ef295 // indirect
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.2.0
@@ -28,7 +28,7 @@ require (
gocloud.dev v0.40.0
golang.org/x/sync v0.11.0
google.golang.org/grpc v1.70.0
google.golang.org/protobuf v1.36.4
google.golang.org/protobuf v1.36.5
k8s.io/apimachinery v0.32.1
)
@@ -82,7 +82,7 @@ require (
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/elazarl/goproxy v1.7.0 // indirect
github.com/elazarl/goproxy v1.7.1 // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/fatih/color v1.17.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
@@ -110,7 +110,7 @@ require (
github.com/google/btree v1.1.3 // indirect
github.com/google/flatbuffers v24.3.25+incompatible // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/s2a-go v0.1.8 // indirect
github.com/google/wire v0.6.0 // indirect
+8 -7
View File
@@ -216,8 +216,8 @@ github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5m
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/elazarl/goproxy v1.7.0 h1:EXv2nV4EjM60ZtsEVLYJG4oBXhDGutMKperpHsZ/v+0=
github.com/elazarl/goproxy v1.7.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ=
github.com/elazarl/goproxy v1.7.1 h1:1P7LPSxbqtNxusFnXclj6O56pjfq1xOQZ6a0mwwKUlY=
github.com/elazarl/goproxy v1.7.1/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@@ -361,8 +361,9 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/go-replayers/grpcreplay v1.3.0 h1:1Keyy0m1sIpqstQmgz307zhiJ1pV4uIlFds5weTmxbo=
@@ -413,8 +414,8 @@ github.com/grafana/grafana-aws-sdk v0.31.5 h1:4HpMQx7n4Qqoi7Bgu8KHQ2QKT9fYYdHilX
github.com/grafana/grafana-aws-sdk v0.31.5/go.mod h1:5p4Cjyr5ZiR6/RT2nFWkJ8XpIKgX4lAUmUMu70m2yCM=
github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6 h1:OfCkitCuomzZKW1WYHrG8MxKwtMhALb7jqoj+487eTg=
github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6/go.mod h1:V7y2BmsWxS3A9Ohebwn4OiSfJJqi//4JQydQ8fHTduo=
github.com/grafana/grafana-plugin-sdk-go v0.266.0 h1:YP+iEpXH3HRX9Xo4NHjsrJhN2W7uVTtkLNzMHYbmiLI=
github.com/grafana/grafana-plugin-sdk-go v0.266.0/go.mod h1:bxkXrBQ4QSmOncsWdIOcpgP+M6wajQNMAPXlbWrqAWY=
github.com/grafana/grafana-plugin-sdk-go v0.267.0 h1:4AcuLEE0UeFG0Eo3P8r1FDS89hgZZ73K550DRz1fgjQ=
github.com/grafana/grafana-plugin-sdk-go v0.267.0/go.mod h1:OuwS4c/JYgn0rr/w5zhJBpLo4gKm/vw15RsfpYAvK9Q=
github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8=
github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls=
github.com/grafana/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKtUuKQbJqgAIjlnicKg=
@@ -1021,8 +1022,8 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
@@ -1,6 +1,7 @@
package alerting
import (
"encoding/json"
"net/http"
"testing"
"time"
@@ -8,6 +9,7 @@ import (
prommodel "github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/expr"
"github.com/grafana/grafana/pkg/services/datasources"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/org"
@@ -97,178 +99,315 @@ var (
)
func TestIntegrationConvertPrometheusEndpoints(t *testing.T) {
testinfra.SQLiteIntegrationTest(t)
runTest := func(t *testing.T, enableLokiPaths bool) {
testinfra.SQLiteIntegrationTest(t)
// Setup Grafana and its Database
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
DisableLegacyAlerting: true,
EnableUnifiedAlerting: true,
DisableAnonymous: true,
AppModeProduction: true,
EnableFeatureToggles: []string{"alertingConversionAPI"},
// Setup Grafana and its Database
dir, gpath := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
DisableLegacyAlerting: true,
EnableUnifiedAlerting: true,
DisableAnonymous: true,
AppModeProduction: true,
EnableFeatureToggles: []string{"alertingConversionAPI"},
})
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, gpath)
// Create users to make authenticated requests
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleAdmin),
Password: "password",
Login: "admin",
})
apiClient := newAlertingApiClient(grafanaListedAddr, "admin", "password")
apiClient.prometheusConversionUseLokiPaths = enableLokiPaths
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleViewer),
Password: "password",
Login: "viewer",
})
viewerClient := newAlertingApiClient(grafanaListedAddr, "viewer", "password")
namespace1 := "test-namespace-1"
namespace2 := "test-namespace-2"
ds := apiClient.CreateDatasource(t, datasources.DS_PROMETHEUS)
t.Run("create rule groups and get them back", func(t *testing.T) {
_, status, body := apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, nil)
requireStatusCode(t, http.StatusAccepted, status, body)
_, status, body = apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup2, nil)
requireStatusCode(t, http.StatusAccepted, status, body)
// create a third group in a different namespace
_, status, body = apiClient.ConvertPrometheusPostRuleGroup(t, namespace2, ds.Body.Datasource.UID, promGroup3, nil)
requireStatusCode(t, http.StatusAccepted, status, body)
// And a non-provisioned rule in another namespace
namespace3UID := util.GenerateShortUID()
apiClient.CreateFolder(t, namespace3UID, "folder")
createRule(t, apiClient, namespace3UID)
// Now get the first group
group1 := apiClient.ConvertPrometheusGetRuleGroupRules(t, namespace1, promGroup1.Name)
require.Equal(t, promGroup1, group1)
// Get namespace1
ns1 := apiClient.ConvertPrometheusGetNamespaceRules(t, namespace1)
expectedNs1 := map[string][]apimodels.PrometheusRuleGroup{
namespace1: {promGroup1, promGroup2},
}
require.Equal(t, expectedNs1, ns1)
// Get all namespaces
namespaces := apiClient.ConvertPrometheusGetAllRules(t)
expectedNamespaces := map[string][]apimodels.PrometheusRuleGroup{
namespace1: {promGroup1, promGroup2},
namespace2: {promGroup3},
}
require.Equal(t, expectedNamespaces, namespaces)
})
t.Run("without permissions to create folders cannot create rule groups either", func(t *testing.T) {
_, status, raw := viewerClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, nil)
requireStatusCode(t, http.StatusForbidden, status, raw)
})
t.Run("delete one rule group", func(t *testing.T) {
_, status, body := apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, nil)
requireStatusCode(t, http.StatusAccepted, status, body)
_, status, body = apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup2, nil)
requireStatusCode(t, http.StatusAccepted, status, body)
_, status, body = apiClient.ConvertPrometheusPostRuleGroup(t, namespace2, ds.Body.Datasource.UID, promGroup3, nil)
requireStatusCode(t, http.StatusAccepted, status, body)
apiClient.ConvertPrometheusDeleteRuleGroup(t, namespace1, promGroup1.Name)
// Check that the promGroup2 and promGroup3 are still there
namespaces := apiClient.ConvertPrometheusGetAllRules(t)
expectedNamespaces := map[string][]apimodels.PrometheusRuleGroup{
namespace1: {promGroup2},
namespace2: {promGroup3},
}
require.Equal(t, expectedNamespaces, namespaces)
// Delete the second namespace
apiClient.ConvertPrometheusDeleteNamespace(t, namespace2)
// Check that only the first namespace is left
namespaces = apiClient.ConvertPrometheusGetAllRules(t)
expectedNamespaces = map[string][]apimodels.PrometheusRuleGroup{
namespace1: {promGroup2},
}
require.Equal(t, expectedNamespaces, namespaces)
})
}
t.Run("with the mimirtool paths", func(t *testing.T) {
runTest(t, false)
})
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)
// Create users to make authenticated requests
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleAdmin),
Password: "password",
Login: "admin",
t.Run("with the cortextool Loki paths", func(t *testing.T) {
runTest(t, true)
})
apiClient := newAlertingApiClient(grafanaListedAddr, "admin", "password")
}
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleViewer),
Password: "password",
Login: "viewer",
})
viewerClient := newAlertingApiClient(grafanaListedAddr, "viewer", "password")
func TestIntegrationConvertPrometheusEndpoints_Conflict(t *testing.T) {
runTest := func(t *testing.T, enableLokiPaths bool) {
testinfra.SQLiteIntegrationTest(t)
namespace1 := "test-namespace-1"
namespace2 := "test-namespace-2"
// Setup Grafana and its Database
dir, gpath := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
DisableLegacyAlerting: true,
EnableUnifiedAlerting: true,
DisableAnonymous: true,
AppModeProduction: true,
EnableFeatureToggles: []string{"alertingConversionAPI"},
})
ds := apiClient.CreateDatasource(t, datasources.DS_PROMETHEUS)
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, gpath)
t.Run("create rule groups and get them back", func(t *testing.T) {
_, status, body := apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, nil)
requireStatusCode(t, http.StatusAccepted, status, body)
_, status, body = apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup2, nil)
requireStatusCode(t, http.StatusAccepted, status, body)
// Create users to make authenticated requests
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleAdmin),
Password: "password",
Login: "admin",
})
apiClient := newAlertingApiClient(grafanaListedAddr, "admin", "password")
apiClient.prometheusConversionUseLokiPaths = enableLokiPaths
// create a third group in a different namespace
_, status, body = apiClient.ConvertPrometheusPostRuleGroup(t, namespace2, ds.Body.Datasource.UID, promGroup3, nil)
requireStatusCode(t, http.StatusAccepted, status, body)
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleViewer),
Password: "password",
Login: "viewer",
})
// And a non-provisioned rule in another namespace
namespace3UID := util.GenerateShortUID()
apiClient.CreateFolder(t, namespace3UID, "folder")
createRule(t, apiClient, namespace3UID)
namespace1 := "test-namespace-1"
// Now get the first group
group1 := apiClient.ConvertPrometheusGetRuleGroupRules(t, namespace1, promGroup1.Name)
require.Equal(t, promGroup1, group1)
ds := apiClient.CreateDatasource(t, datasources.DS_PROMETHEUS)
// Get namespace1
ns1 := apiClient.ConvertPrometheusGetNamespaceRules(t, namespace1)
expectedNs1 := map[string][]apimodels.PrometheusRuleGroup{
namespace1: {promGroup1, promGroup2},
}
require.Equal(t, expectedNs1, ns1)
t.Run("cannot overwrite a rule group with different provenance", func(t *testing.T) {
// Create a rule group using the provisioning API and then try to overwrite it
// using the Prometheus Conversion API. It should fail because the provenance
// we set for rules in these two APIs is different and we check that when updating.
provisionedRuleGroup := apimodels.AlertRuleGroup{
Title: promGroup1.Name,
Interval: 60,
FolderUID: namespace1,
Rules: []apimodels.ProvisionedAlertRule{
{
Title: "Rule1",
OrgID: 1,
RuleGroup: promGroup1.Name,
Condition: "A",
NoDataState: apimodels.Alerting,
ExecErrState: apimodels.AlertingErrState,
For: prommodel.Duration(time.Duration(60) * time.Second),
Data: []apimodels.AlertQuery{
{
RefID: "A",
RelativeTimeRange: apimodels.RelativeTimeRange{
From: apimodels.Duration(time.Duration(5) * time.Hour),
To: apimodels.Duration(time.Duration(3) * time.Hour),
},
DatasourceUID: expr.DatasourceUID,
Model: json.RawMessage([]byte(`{"type":"math","expression":"2 + 3 \u003e 1"}`)),
},
},
},
},
}
// Get all namespaces
namespaces := apiClient.ConvertPrometheusGetAllRules(t)
expectedNamespaces := map[string][]apimodels.PrometheusRuleGroup{
namespace1: {promGroup1, promGroup2},
namespace2: {promGroup3},
}
require.Equal(t, expectedNamespaces, namespaces)
// Create the folder
apiClient.CreateFolder(t, namespace1, namespace1)
// Create rule in the root folder using another API
_, status, response := apiClient.CreateOrUpdateRuleGroupProvisioning(t, provisionedRuleGroup)
require.Equalf(t, http.StatusOK, status, response)
// Should fail to post the group
_, status, body := apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, nil)
requireStatusCode(t, http.StatusConflict, status, body)
})
}
t.Run("with the mimirtool paths", func(t *testing.T) {
runTest(t, false)
})
t.Run("without permissions to create folders cannot create rule groups either", func(t *testing.T) {
_, status, raw := viewerClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, nil)
requireStatusCode(t, http.StatusForbidden, status, raw)
t.Run("with the cortextool Loki paths", func(t *testing.T) {
runTest(t, true)
})
}
func TestIntegrationConvertPrometheusEndpoints_CreatePausedRules(t *testing.T) {
testinfra.SQLiteIntegrationTest(t)
runTest := func(t *testing.T, enableLokiPaths bool) {
testinfra.SQLiteIntegrationTest(t)
// Setup Grafana and its Database
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
DisableLegacyAlerting: true,
EnableUnifiedAlerting: true,
DisableAnonymous: true,
AppModeProduction: true,
EnableFeatureToggles: []string{"alertingConversionAPI"},
})
// Setup Grafana and its Database
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
DisableLegacyAlerting: true,
EnableUnifiedAlerting: true,
DisableAnonymous: true,
AppModeProduction: true,
EnableFeatureToggles: []string{"alertingConversionAPI"},
})
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)
// Create users to make authenticated requests
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleAdmin),
Password: "password",
Login: "admin",
})
apiClient := newAlertingApiClient(grafanaListedAddr, "admin", "password")
// Create users to make authenticated requests
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleAdmin),
Password: "password",
Login: "admin",
})
apiClient := newAlertingApiClient(grafanaListedAddr, "admin", "password")
ds := apiClient.CreateDatasource(t, datasources.DS_PROMETHEUS)
ds := apiClient.CreateDatasource(t, datasources.DS_PROMETHEUS)
namespace1 := "test-namespace-1"
namespace1 := "test-namespace-1"
namespace1UID := util.GenerateShortUID()
apiClient.CreateFolder(t, namespace1UID, namespace1)
namespace1UID := util.GenerateShortUID()
apiClient.CreateFolder(t, namespace1UID, namespace1)
t.Run("when pausing header is set, rules should be paused", func(t *testing.T) {
tests := []struct {
name string
recordingPaused bool
alertPaused bool
}{
{
name: "do not pause rules",
recordingPaused: false,
alertPaused: false,
},
{
name: "pause recording rules",
recordingPaused: true,
alertPaused: false,
},
{
name: "pause alert rules",
recordingPaused: false,
alertPaused: true,
},
{
name: "pause both recording and alert rules",
recordingPaused: true,
alertPaused: true,
},
}
t.Run("when pausing header is set, rules should be paused", func(t *testing.T) {
tests := []struct {
name string
recordingPaused bool
alertPaused bool
}{
{
name: "do not pause rules",
recordingPaused: false,
alertPaused: false,
},
{
name: "pause recording rules",
recordingPaused: true,
alertPaused: false,
},
{
name: "pause alert rules",
recordingPaused: false,
alertPaused: true,
},
{
name: "pause both recording and alert rules",
recordingPaused: true,
alertPaused: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
headers := map[string]string{}
if tc.recordingPaused {
headers["X-Grafana-Alerting-Recording-Rules-Paused"] = "true"
}
if tc.alertPaused {
headers["X-Grafana-Alerting-Alert-Rules-Paused"] = "true"
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
headers := map[string]string{}
if tc.recordingPaused {
headers["X-Grafana-Alerting-Recording-Rules-Paused"] = "true"
}
if tc.alertPaused {
headers["X-Grafana-Alerting-Alert-Rules-Paused"] = "true"
}
apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, headers)
apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, headers)
gr, _, _ := apiClient.GetRulesGroupWithStatus(t, namespace1UID, promGroup1.Name)
gr, _, _ := apiClient.GetRulesGroupWithStatus(t, namespace1UID, promGroup1.Name)
require.Len(t, gr.Rules, 3)
require.Len(t, gr.Rules, 3)
pausedRecordingRules := 0
pausedAlertRules := 0
pausedRecordingRules := 0
pausedAlertRules := 0
for _, rule := range gr.Rules {
if rule.GrafanaManagedAlert.IsPaused {
if rule.GrafanaManagedAlert.Record != nil {
pausedRecordingRules++
} else {
pausedAlertRules++
for _, rule := range gr.Rules {
if rule.GrafanaManagedAlert.IsPaused {
if rule.GrafanaManagedAlert.Record != nil {
pausedRecordingRules++
} else {
pausedAlertRules++
}
}
}
}
if tc.recordingPaused {
require.Equal(t, 1, pausedRecordingRules)
} else {
require.Equal(t, 0, pausedRecordingRules)
}
if tc.recordingPaused {
require.Equal(t, 1, pausedRecordingRules)
} else {
require.Equal(t, 0, pausedRecordingRules)
}
if tc.alertPaused {
require.Equal(t, 2, pausedAlertRules)
} else {
require.Equal(t, 0, pausedAlertRules)
}
})
}
if tc.alertPaused {
require.Equal(t, 2, pausedAlertRules)
} else {
require.Equal(t, 0, pausedAlertRules)
}
})
}
})
}
t.Run("with the mimirtool paths", func(t *testing.T) {
runTest(t, false)
})
t.Run("with the cortextool Loki paths", func(t *testing.T) {
runTest(t, true)
})
}
+51 -4
View File
@@ -254,7 +254,8 @@ func convertGettableGrafanaRuleToPostable(gettable *apimodels.GettableGrafanaRul
}
type apiClient struct {
url string
url string
prometheusConversionUseLokiPaths bool
}
type LegacyApiClient struct {
@@ -1121,7 +1122,13 @@ func (a apiClient) ConvertPrometheusPostRuleGroup(t *testing.T, namespaceTitle,
func (a apiClient) ConvertPrometheusGetRuleGroupRules(t *testing.T, namespaceTitle, groupName string) apimodels.PrometheusRuleGroup {
t.Helper()
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/convert/prometheus/config/v1/rules/%s/%s", a.url, namespaceTitle, groupName), nil)
path := "%s/api/convert/prometheus/config/v1/rules/%s/%s"
if a.prometheusConversionUseLokiPaths {
path = "%s/api/convert/api/prom/rules/%s/%s"
}
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf(path, a.url, namespaceTitle, groupName), nil)
require.NoError(t, err)
rule, status, raw := sendRequestYAML[apimodels.PrometheusRuleGroup](t, req, http.StatusOK)
requireStatusCode(t, http.StatusOK, status, raw)
@@ -1130,7 +1137,13 @@ func (a apiClient) ConvertPrometheusGetRuleGroupRules(t *testing.T, namespaceTit
func (a apiClient) ConvertPrometheusGetNamespaceRules(t *testing.T, namespaceTitle string) map[string][]apimodels.PrometheusRuleGroup {
t.Helper()
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/convert/prometheus/config/v1/rules/%s", a.url, namespaceTitle), nil)
path := "%s/api/convert/prometheus/config/v1/rules/%s"
if a.prometheusConversionUseLokiPaths {
path = "%s/api/convert/api/prom/rules/%s"
}
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf(path, a.url, namespaceTitle), nil)
require.NoError(t, err)
ns, status, raw := sendRequestYAML[map[string][]apimodels.PrometheusRuleGroup](t, req, http.StatusOK)
requireStatusCode(t, http.StatusOK, status, raw)
@@ -1139,13 +1152,47 @@ func (a apiClient) ConvertPrometheusGetNamespaceRules(t *testing.T, namespaceTit
func (a apiClient) ConvertPrometheusGetAllRules(t *testing.T) map[string][]apimodels.PrometheusRuleGroup {
t.Helper()
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/convert/prometheus/config/v1/rules", a.url), nil)
path := "%s/api/convert/prometheus/config/v1/rules"
if a.prometheusConversionUseLokiPaths {
path = "%s/api/convert/api/prom/rules"
}
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf(path, a.url), nil)
require.NoError(t, err)
result, status, raw := sendRequestYAML[map[string][]apimodels.PrometheusRuleGroup](t, req, http.StatusOK)
requireStatusCode(t, http.StatusOK, status, raw)
return result
}
func (a apiClient) ConvertPrometheusDeleteRuleGroup(t *testing.T, namespaceTitle, groupName string) {
t.Helper()
path := "%s/api/convert/prometheus/config/v1/rules/%s/%s"
if a.prometheusConversionUseLokiPaths {
path = "%s/api/convert/api/prom/rules/%s/%s"
}
req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf(path, a.url, namespaceTitle, groupName), nil)
require.NoError(t, err)
_, status, raw := sendRequestJSON[apimodels.ConvertPrometheusResponse](t, req, http.StatusAccepted)
requireStatusCode(t, http.StatusAccepted, status, raw)
}
func (a apiClient) ConvertPrometheusDeleteNamespace(t *testing.T, namespaceTitle string) {
t.Helper()
path := "%s/api/convert/prometheus/config/v1/rules/%s"
if a.prometheusConversionUseLokiPaths {
path = "%s/api/convert/api/prom/rules/%s"
}
req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf(path, a.url, namespaceTitle), nil)
require.NoError(t, err)
_, status, raw := sendRequestJSON[apimodels.ConvertPrometheusResponse](t, req, http.StatusAccepted)
requireStatusCode(t, http.StatusAccepted, status, raw)
}
func sendRequestRaw(t *testing.T, req *http.Request) ([]byte, int, error) {
t.Helper()
client := &http.Client{}
@@ -5,7 +5,7 @@ import { Props } from 'react-virtualized-auto-sizer';
import { render, screen, waitFor, within } from 'test/test-utils';
import { byLabelText, byRole } from 'testing-library-selector';
import { CodeEditorProps } from '@grafana/ui/src/components/Monaco/types';
import { CodeEditor } from '@grafana/ui';
import { AppNotificationList } from 'app/core/components/AppNotifications/AppNotificationList';
import { setupMswServer } from 'app/features/alerting/unified/mockApi';
import { testWithFeatureToggles } from 'app/features/alerting/unified/test/test-utils';
@@ -14,6 +14,8 @@ import { AccessControlAction } from 'app/types';
import Templates from './Templates';
import { grantUserPermissions } from './mocks';
type CodeEditorProps = React.ComponentProps<typeof CodeEditor>;
jest.mock('app/core/components/AppChrome/AppChromeUpdate', () => ({
AppChromeUpdate: ({ actions }: { actions: React.ReactNode }) => <div>{actions}</div>,
}));
@@ -4,10 +4,10 @@ import { PluginExtensionLink, PluginExtensionPoints } from '@grafana/data';
import { usePluginLinks } from '@grafana/runtime';
import { Dropdown, IconButton } from '@grafana/ui';
import { ConfirmNavigationModal } from 'app/features/explore/extensions/ConfirmNavigationModal';
// We might want to customise this in future but right now the toolbar menu from the Explore view is fine.
import { ToolbarExtensionPointMenu as AlertExtensionPointMenu } from 'app/features/explore/extensions/ToolbarExtensionPointMenu';
import { Alert, CombinedRule } from 'app/types/unified-alerting';
import { AlertExtensionPointMenu } from './AlertInstanceExtensionPointMenu';
interface AlertInstanceExtensionPointProps {
rule?: CombinedRule;
instance: Alert;
@@ -1,2 +0,0 @@
// We might want to customise this in future but right now the toolbar menu from the Explore view is fine.
export { ToolbarExtensionPointMenu as AlertExtensionPointMenu } from 'app/features/explore/extensions/ToolbarExtensionPointMenu';
@@ -4,16 +4,15 @@
* It includes auto-complete for template data and syntax highlighting
*/
import { IDisposable, editor } from 'monaco-editor';
import { useEffect, useRef } from 'react';
import { ComponentProps, useEffect, useRef } from 'react';
import { CodeEditor } from '@grafana/ui';
import { CodeEditorProps } from '@grafana/ui/src/components/Monaco/types';
import { registerGoTemplateAutocomplete } from './editor/autocomplete';
import goTemplateLanguageDefinition, { GO_TEMPLATE_LANGUAGE_ID } from './editor/definition';
import { registerLanguage } from './editor/register';
type TemplateEditorProps = Omit<CodeEditorProps, 'language' | 'theme'> & {
type TemplateEditorProps = Omit<ComponentProps<typeof CodeEditor>, 'language' | 'theme'> & {
autoHeight?: boolean;
};
@@ -1,8 +1,9 @@
import { css } from '@emotion/css';
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Button, Input, useStyles2 } from '@grafana/ui';
import { Button, Input, Stack, useStyles2 } from '@grafana/ui';
import { Trans, t } from 'app/core/internationalization';
import { ActionIcon } from '../../../rules/ActionIcon';
@@ -15,7 +16,7 @@ interface Props {
export const KeyValueMapInput = ({ value, onChange, readOnly = false }: Props) => {
const styles = useStyles2(getStyles);
const [pairs, setPairs] = useState(recordToPairs(value));
useEffect(() => setPairs(recordToPairs(value)), [value]);
const [currentNewPair, setCurrentNewPair] = useState<[string, string] | undefined>(undefined);
const emitChange = (pairs: Array<[string, string]>) => {
onChange(pairsToRecord(pairs));
@@ -30,15 +31,6 @@ export const KeyValueMapInput = ({ value, onChange, readOnly = false }: Props) =
}
};
const updatePair = (values: [string, string], index: number) => {
const old = pairs[index];
const newPairs = pairs.map((pair, i) => (i === index ? values : pair));
setPairs(newPairs);
if (values[0] || old[0]) {
emitChange(newPairs);
}
};
return (
<div>
{!!pairs.length && (
@@ -54,22 +46,18 @@ export const KeyValueMapInput = ({ value, onChange, readOnly = false }: Props) =
{pairs.map(([key, value], index) => (
<tr key={index}>
<td>
<Input
readOnly={readOnly}
value={key}
onChange={(e) => updatePair([e.currentTarget.value, value], index)}
/>
<Input readOnly={readOnly} value={key} disabled />
</td>
<td>
<Input
readOnly={readOnly}
value={value}
onChange={(e) => updatePair([key, e.currentTarget.value], index)}
/>
<Input readOnly={readOnly} value={value} disabled />
</td>
{!readOnly && (
<td>
<ActionIcon icon="trash-alt" tooltip="delete" onClick={() => deleteItem(index)} />
<ActionIcon
icon="trash-alt"
tooltip={t('alerting.common.delete', 'Delete')}
onClick={() => deleteItem(index)}
/>
</td>
)}
</tr>
@@ -77,6 +65,44 @@ export const KeyValueMapInput = ({ value, onChange, readOnly = false }: Props) =
</tbody>
</table>
)}
{currentNewPair && (
<table className={styles.table}>
<tr>
<Stack gap={1}>
<td>
<Input
value={currentNewPair[0]}
onChange={(e) => setCurrentNewPair([e.currentTarget.value, currentNewPair[1]])}
/>
</td>
<td>
<Input
value={currentNewPair[1]}
onChange={(e) => setCurrentNewPair([currentNewPair[0], e.currentTarget.value])}
/>
</td>
<td>
<Stack gap={1}>
<ActionIcon
icon="check"
tooltip={t('alerting.contact-points.key-value-map.confirm-add', 'Confirm to add')}
onClick={() => {
setPairs([...pairs, currentNewPair]);
setCurrentNewPair(undefined);
emitChange([...pairs, currentNewPair]);
}}
/>
<ActionIcon
icon="times"
tooltip={t('alerting.common.cancel', 'Cancel')}
onClick={() => setCurrentNewPair(undefined)}
/>
</Stack>
</td>
</Stack>
</tr>
</table>
)}
{!readOnly && (
<Button
className={styles.addButton}
@@ -84,9 +110,10 @@ export const KeyValueMapInput = ({ value, onChange, readOnly = false }: Props) =
variant="secondary"
icon="plus"
size="sm"
onClick={() => setPairs([...pairs, ['', '']])}
disabled={!!currentNewPair}
onClick={() => setCurrentNewPair(['', ''])}
>
Add
<Trans i18nKey="alerting.contact-points.key-value-map.add">Add</Trans>
</Button>
)}
</div>
@@ -1,7 +1,7 @@
import { ReactNode } from 'react';
import { ComponentProps, ReactNode } from 'react';
import { render, screen, userEvent } from 'test/test-utils';
import { CodeEditorProps } from '@grafana/ui/src/components/Monaco/types';
import { CodeEditor } from '@grafana/ui';
import { setupMswServer } from 'app/features/alerting/unified/mockApi';
import { grantUserPermissions } from 'app/features/alerting/unified/mocks';
import { getAlertmanagerConfig } from 'app/features/alerting/unified/mocks/server/entities/alertmanagers';
@@ -15,6 +15,8 @@ import { AccessControlAction, NotificationChannelOption } from 'app/types';
import { TemplatesPicker, getTemplateOptions } from './TemplateSelector';
import { parseTemplates } from './utils';
type CodeEditorProps = ComponentProps<typeof CodeEditor>;
const alertmanagerConfigMock = getAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME);
jest.mock('@grafana/ui', () => ({
@@ -168,7 +168,7 @@ export const getPayloadToExport = (
} else {
// we have to create a new group with the updated rule
return {
name: existingGroup?.name ?? '',
name: existingGroup?.name ?? formValues.group,
rules: [updatedRule],
};
}

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