merge main

This commit is contained in:
Ryan McKinley
2025-06-27 12:49:54 -07:00
864 changed files with 67818 additions and 25883 deletions
+7 -10
View File
@@ -4,7 +4,7 @@ go 1.24.4
require (
github.com/emicklei/go-restful/v3 v3.11.0
github.com/grafana/grafana-plugin-sdk-go v0.277.0
github.com/grafana/grafana-plugin-sdk-go v0.278.0
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e
github.com/grafana/grafana/pkg/semconv v0.0.0-20250514132646-acbc7b54ed9e
github.com/mattbaird/jsonpatch v0.0.0-20240118010651-0ba75a80ca38
@@ -25,13 +25,13 @@ require (
github.com/BurntSushi/toml v1.5.0 // indirect
github.com/NYTimes/gziphandler v1.1.1 // indirect
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
github.com/apache/arrow-go/v18 v18.2.0 // indirect
github.com/apache/arrow-go/v18 v18.3.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cheekybits/genny v1.0.0 // indirect
github.com/chromedp/cdproto v0.0.0-20240810084448-b931b754e476 // indirect
github.com/chromedp/cdproto v0.0.0-20250429231605-6ed5b53462d4 // indirect
github.com/coreos/go-semver v0.3.1 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
@@ -53,7 +53,6 @@ require (
github.com/gogo/googleapis v1.4.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v1.0.0 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/cel-go v0.25.0 // indirect
github.com/google/flatbuffers v25.2.10+incompatible // indirect
@@ -65,7 +64,7 @@ require (
github.com/grafana/otel-profiling-go v0.5.1 // indirect
github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 // indirect
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // indirect
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 // indirect
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
@@ -125,15 +124,14 @@ require (
go.etcd.io/etcd/client/v3 v3.5.21 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
go.opentelemetry.io/contrib/propagators/jaeger v1.35.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.61.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/contrib/propagators/jaeger v1.36.0 // indirect
go.opentelemetry.io/contrib/samplers/jaegerremote v0.30.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 // indirect
go.opentelemetry.io/otel/metric v1.36.0 // indirect
go.opentelemetry.io/otel/sdk v1.36.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.36.0 // indirect
go.opentelemetry.io/otel/trace v1.36.0 // indirect
go.opentelemetry.io/proto/otlp v1.6.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
@@ -150,7 +148,6 @@ require (
golang.org/x/time v0.11.0 // indirect
golang.org/x/tools v0.34.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
gonum.org/v1/gonum v0.16.0 // indirect
google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect
+14 -14
View File
@@ -11,8 +11,8 @@ github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7X
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
github.com/apache/arrow-go/v18 v18.2.0 h1:QhWqpgZMKfWOniGPhbUxrHohWnooGURqL2R2Gg4SO1Q=
github.com/apache/arrow-go/v18 v18.2.0/go.mod h1:Ic/01WSwGJWRrdAZcxjBZ5hbApNJ28K96jGYaxzzGUc=
github.com/apache/arrow-go/v18 v18.3.0 h1:Xq4A6dZj9Nu33sqZibzn012LNnewkTUlfKVUFD/RX/I=
github.com/apache/arrow-go/v18 v18.3.0/go.mod h1:eEM1DnUTHhgGAjf/ChvOAQbUQ+EPohtDrArffvUjPg8=
github.com/apache/thrift v0.21.0 h1:tdPmh/ptjE1IJnhbhrcl2++TauVjy242rkV/UzJChnE=
github.com/apache/thrift v0.21.0/go.mod h1:W1H8aR/QRtYNvrPeFXBtobyRkd0/YVhTc6i07XIAgDw=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
@@ -30,8 +30,8 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE=
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
github.com/chromedp/cdproto v0.0.0-20240810084448-b931b754e476 h1:VnjHsRXCRti7Av7E+j4DCha3kf68echfDzQ+wD11SBU=
github.com/chromedp/cdproto v0.0.0-20240810084448-b931b754e476/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
github.com/chromedp/cdproto v0.0.0-20250429231605-6ed5b53462d4 h1:UZdrvid2JFwnvPlUSEFlE794XZL4Jmrj8fuxfcLECJE=
github.com/chromedp/cdproto v0.0.0-20250429231605-6ed5b53462d4/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
@@ -134,8 +134,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.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
github.com/grafana/grafana-plugin-sdk-go v0.277.0 h1:VDU2F4Y5NeRS//ejctdZtsAshrGaEdbtW33FsK0EQss=
github.com/grafana/grafana-plugin-sdk-go v0.277.0/go.mod h1:mAUWg68w5+1f5TLDqagIr8sWr1RT9h7ufJl5NMcWJAU=
github.com/grafana/grafana-plugin-sdk-go v0.278.0 h1:5/rIYparLi02pofdaag8wnjspMMVNCi8cZhC4cdC3Ho=
github.com/grafana/grafana-plugin-sdk-go v0.278.0/go.mod h1:+8NXT/XUJ/89GV6FxGQ366NZ3nU+cAXDMd0OUESF9H4=
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e h1:BTKk7LHuG1kmAkucwTA7DuMbKpKvJTKrGdBmUNO4dfQ=
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e/go.mod h1:IA4SOwun8QyST9c5UNs/fN37XL6boXXDvRYFcFwbipg=
github.com/grafana/grafana/pkg/semconv v0.0.0-20250514132646-acbc7b54ed9e h1:vheR6iPO1np+G/ARjcWx9yiWd7BnTDgyTgsnMhOvx70=
@@ -146,8 +146,8 @@ github.com/grafana/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKt
github.com/grafana/pyroscope-go/godeltaprof v0.1.8/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI=
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8=
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA=
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU=
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o=
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 h1:sGm2vDRFUrQJO/Veii4h4zG2vvqG6uWNkBHSTqXOZk0=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2/go.mod h1:wd1YpapPLivG6nQgbf7ZkG1hhSOXDhhn4MLTknx2aAc=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 h1:uGoIog/wiQHI9GAxXO5TJbT0wWKH3O9HhOJW1F9c3fY=
@@ -357,12 +357,12 @@ go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJyS
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 h1:0tY123n7CdWMem7MOVdKOt0YfshufLCwfE5Bob+hQuM=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0/go.mod h1:CosX/aS4eHnG9D7nESYpV753l4j9q5j3SL/PUYd2lR8=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
go.opentelemetry.io/contrib/propagators/jaeger v1.35.0 h1:UIrZgRBHUrYRlJ4V419lVb4rs2ar0wFzKNAebaP05XU=
go.opentelemetry.io/contrib/propagators/jaeger v1.35.0/go.mod h1:0ciyFyYZxE6JqRAQvIgGRabKWDUmNdW3GAQb6y/RlFU=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.61.0 h1:lREC4C0ilyP4WibDhQ7Gg2ygAQFP8oR07Fst/5cafwI=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.61.0/go.mod h1:HfvuU0kW9HewH14VCOLImqKvUgONodURG7Alj/IrnGI=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/contrib/propagators/jaeger v1.36.0 h1:SoCgXYF4ISDtNyfLUzsGDaaudZVTx2yJhOyBO0+/GYk=
go.opentelemetry.io/contrib/propagators/jaeger v1.36.0/go.mod h1:VHu48l0YTRKSObdPQ+Sb8xMZvdnJlN7yhHuHoPgNqHM=
go.opentelemetry.io/contrib/samplers/jaegerremote v0.30.0 h1:bQ1Gvah4Sp8z7epSkgJaNTuZm7sutfA6Fji2/7cKFMc=
go.opentelemetry.io/contrib/samplers/jaegerremote v0.30.0/go.mod h1:9b8Q9rH52NgYH3ShiTFB5wf18Vt3RTH/VMB7LDcC1ug=
go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo=
+16 -1
View File
@@ -440,7 +440,9 @@ func TestFolderGetAPIEndpoint(t *testing.T) {
expectedParentOrgIDs: []int64{0, 0},
expectedParentTitles: []string{"parent title", "subfolder title"},
permissions: []accesscontrol.Permission{
{Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersProvider.GetResourceAllScope()},
{Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID("uid")},
{Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID("parent")},
{Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID("subfolder")},
},
},
{
@@ -455,6 +457,19 @@ func TestFolderGetAPIEndpoint(t *testing.T) {
{Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID("uid")},
},
},
{
description: "get folder by UID should return some parent folder titles and some parent folders as redacted if nested folder are enabled and user only has read access to some parent folders",
URL: "/api/folders/uid",
expectedCode: http.StatusOK,
features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders),
expectedParentUIDs: []string{REDACTED, "subfolder"},
expectedParentOrgIDs: []int64{0, 0},
expectedParentTitles: []string{REDACTED, "subfolder title"},
permissions: []accesscontrol.Permission{
{Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID("uid")},
{Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID("subfolder")},
},
},
{
description: "get folder by UID should not return parent folders if nested folder are disabled",
URL: "/api/folders/uid",
+1 -1
View File
@@ -49,7 +49,7 @@ func TestCallResource(t *testing.T) {
cfg.StaticRootPath = staticRootPath
cfg.Azure = &azsettings.AzureSettings{}
coreRegistry := coreplugin.ProvideCoreRegistry(tracing.InitializeTracerForTest(), nil, &cloudwatch.CloudWatchService{}, nil, nil, nil, nil,
coreRegistry := coreplugin.ProvideCoreRegistry(tracing.InitializeTracerForTest(), nil, &cloudwatch.Service{}, nil, nil, nil, nil,
nil, nil, nil, nil, testdatasource.ProvideService(), nil, nil, nil, nil, nil, nil, nil, nil)
testCtx := pluginsintegration.CreateIntegrationTestCtx(t, cfg, coreRegistry)
+1
View File
@@ -125,6 +125,7 @@ var serviceIdentityTokenPermissions = getTokenPermissions(
"dashboard.grafana.app",
"secret.grafana.app",
"query.grafana.app",
"iam.grafana.app",
)
var ServiceIdentityClaims = &authn.Claims[authn.AccessTokenClaims]{
-6
View File
@@ -1,6 +0,0 @@
// +k8s:deepcopy-gen=package
// +k8s:openapi-gen=true
// +k8s:defaulter-gen=TypeMeta
// +groupName=query.grafana.app
package template // import "github.com/grafana/grafana/pkg/apis/query/v0alpha1/template"
@@ -1,68 +0,0 @@
package template
import (
"bytes"
"encoding/csv"
"encoding/json"
"strings"
)
func FormatVariables(fmt VariableFormat, input []string) string {
if len(input) < 1 {
return ""
}
// MultiValued formats
// nolint: exhaustive
switch fmt {
case FormatJSON:
v, _ := json.Marshal(input)
return string(v)
case FormatDoubleQuote:
sb := bytes.NewBufferString("")
for idx, val := range input {
if idx > 0 {
_, _ = sb.WriteRune(',')
}
_, _ = sb.WriteRune('"')
_, _ = sb.WriteString(strings.ReplaceAll(val, `"`, `\"`))
_, _ = sb.WriteRune('"')
}
return sb.String()
case FormatSingleQuote:
sb := bytes.NewBufferString("")
for idx, val := range input {
if idx > 0 {
_, _ = sb.WriteRune(',')
}
_, _ = sb.WriteRune('\'')
_, _ = sb.WriteString(strings.ReplaceAll(val, `'`, `\'`))
_, _ = sb.WriteRune('\'')
}
return sb.String()
case FormatCSV:
sb := bytes.NewBufferString("")
w := csv.NewWriter(sb)
_ = w.Write(input)
w.Flush()
v := sb.Bytes()
return string(v[:len(v)-1])
}
// Single valued formats
if len(input) == 1 {
return input[0]
}
// nolint: exhaustive
switch fmt {
case FormatPipe:
return strings.Join(input, "|")
}
// Raw output (joined with a comma)
return strings.Join(input, ",")
}
@@ -1,81 +0,0 @@
package template
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestFormat(t *testing.T) {
// Invalid input
require.Equal(t, "", FormatVariables(FormatCSV, nil))
require.Equal(t, "", FormatVariables(FormatCSV, []string{}))
type check struct {
name string
input []string
output map[VariableFormat]string
}
tests := []check{
{
name: "three simple variables",
input: []string{"a", "b", "c"},
output: map[VariableFormat]string{
FormatCSV: "a,b,c",
FormatJSON: `["a","b","c"]`,
FormatDoubleQuote: `"a","b","c"`,
FormatSingleQuote: `'a','b','c'`,
FormatPipe: `a|b|c`,
FormatRaw: "a,b,c",
},
},
{
name: "single value",
input: []string{"a"},
output: map[VariableFormat]string{
FormatCSV: "a",
FormatJSON: `["a"]`,
FormatDoubleQuote: `"a"`,
FormatSingleQuote: `'a'`,
FormatPipe: "a",
FormatRaw: "a",
},
},
{
name: "value with quote",
input: []string{`hello "world"`},
output: map[VariableFormat]string{
FormatCSV: `"hello ""world"""`, // note the double quotes
FormatJSON: `["hello \"world\""]`,
FormatDoubleQuote: `"hello \"world\""`,
FormatSingleQuote: `'hello "world"'`,
FormatPipe: `hello "world"`,
FormatRaw: `hello "world"`,
},
},
}
for _, test := range tests {
// Make sure all keys are set in tests
all := map[VariableFormat]bool{
FormatRaw: true,
FormatCSV: true,
FormatJSON: true,
FormatDoubleQuote: true,
FormatSingleQuote: true,
FormatPipe: true,
}
// Check the default (no format) matches CSV
require.Equal(t, test.output[FormatRaw],
FormatVariables("", test.input),
"test %s default values are not raw", test.name)
// Check each input value
for format, v := range test.output {
require.Equal(t, v, FormatVariables(format, test.input), "Test: %s (format:%s)", test.name, format)
delete(all, format)
}
require.Empty(t, all, "test %s is missing cases for: %v", test.name, all)
}
}
-119
View File
@@ -1,119 +0,0 @@
package template
import (
"fmt"
"sort"
data "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1"
"github.com/spyzhov/ajson"
)
// RenderTemplate applies selected values into a query template
func RenderTemplate(qt QueryTemplate, selectedValues map[string][]string) ([]Target, error) {
targets := qt.DeepCopy().Targets
rawTargetObjects := make([]*ajson.Node, len(qt.Targets))
for i, t := range qt.Targets {
b, err := t.Properties.MarshalJSON()
if err != nil {
return nil, err
}
rawTargetObjects[i], err = ajson.Unmarshal(b)
if err != nil {
return nil, err
}
}
rm := getReplacementMap(qt)
for targetIdx, byTargetIdx := range rm {
for path, reps := range byTargetIdx {
o := rawTargetObjects[targetIdx]
nodes, err := o.JSONPath(path)
if err != nil {
return nil, fmt.Errorf("failed to find path %v: %w", path, err)
}
if len(nodes) != 1 {
return nil, fmt.Errorf("expected one lead node at path %v but got %v", path, len(nodes))
}
n := nodes[0]
if !n.IsString() {
return nil, fmt.Errorf("only string type leaf notes supported currently, %v is not a string", path)
}
s := []rune(n.String())
s = s[1 : len(s)-1]
var offSet int64
for _, r := range reps {
sV := selectedValues[r.Key]
if sV == nil {
sV = r.DefaultValues
}
value := []rune(FormatVariables(r.format, sV))
if r.Position == nil {
return nil, fmt.Errorf("nil position not support yet, will be full replacement")
}
s = append(s[:r.Start+offSet], append(value, s[r.End+offSet:]...)...)
offSet += int64(len(value)) - (r.End - r.Start)
}
if err = n.SetString(string(s)); err != nil {
return nil, err
}
}
}
for i, aT := range rawTargetObjects {
raw, err := ajson.Marshal(aT)
if err != nil {
return nil, err
}
u := data.DataQuery{}
err = u.UnmarshalJSON(raw)
if err != nil {
return nil, err
}
targets[i].Properties = u
}
return targets, nil
}
type replacement struct {
*Position
*TemplateVariable
format VariableFormat
}
func getReplacementMap(qt QueryTemplate) map[int]map[string][]replacement {
byTargetPath := make(map[int]map[string][]replacement)
varMap := make(map[string]*TemplateVariable, len(qt.Variables))
for i, v := range qt.Variables {
varMap[v.Key] = &qt.Variables[i]
}
for i, target := range qt.Targets {
if byTargetPath[i] == nil {
byTargetPath[i] = make(map[string][]replacement)
}
for k, vReps := range target.Variables {
for rI, rep := range vReps {
byTargetPath[i][rep.Path] = append(byTargetPath[i][rep.Path],
replacement{
Position: vReps[rI].Position,
TemplateVariable: varMap[k],
format: rep.Format,
},
)
}
}
}
for idx, byTargetIdx := range byTargetPath {
for path := range byTargetIdx {
sort.Slice(byTargetPath[idx][path], func(i, j int) bool {
return byTargetPath[idx][path][i].Start < byTargetPath[idx][path][j].Start
})
}
}
return byTargetPath
}
@@ -1,244 +0,0 @@
package template
import (
"testing"
"github.com/grafana/grafana-plugin-sdk-go/data"
apidata "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1"
"github.com/stretchr/testify/require"
)
var nestedFieldRender = QueryTemplate{
Title: "Test",
Variables: []TemplateVariable{
{
Key: "metricName",
DefaultValues: []string{"cow_count"},
},
},
Targets: []Target{
{
DataType: data.FrameTypeUnknown,
//DataTypeVersion: data.FrameTypeVersion{0, 0},
Variables: map[string][]VariableReplacement{
"metricName": {
{
Path: "$.nestedObject.anArray[0]",
Position: &Position{
Start: 0,
End: 3,
},
},
},
},
Properties: apidata.NewDataQuery(map[string]any{
"nestedObject": map[string]any{
"anArray": []any{"foo", .2},
},
}),
},
},
}
var nestedFieldRenderedTargets = []Target{
{
DataType: data.FrameTypeUnknown,
Variables: map[string][]VariableReplacement{
"metricName": {
{
Path: "$.nestedObject.anArray[0]",
Position: &Position{
Start: 0,
End: 3,
},
},
},
},
//DataTypeVersion: data.FrameTypeVersion{0, 0},
Properties: apidata.NewDataQuery(
map[string]any{
"nestedObject": map[string]any{
"anArray": []any{"up", .2},
},
}),
},
}
var nestedFieldDefaultRenderedTargets = []Target{
{
DataType: data.FrameTypeUnknown,
Variables: map[string][]VariableReplacement{
"metricName": {
{
Path: "$.nestedObject.anArray[0]",
Position: &Position{
Start: 0,
End: 3,
},
},
},
},
//DataTypeVersion: data.FrameTypeVersion{0, 0},
Properties: apidata.NewDataQuery(
map[string]any{
"nestedObject": map[string]any{
"anArray": []any{"cow_count", .2},
},
}),
},
}
func TestNestedFieldRender(t *testing.T) {
rT, err := RenderTemplate(nestedFieldRender, map[string][]string{"metricName": {"up"}})
require.NoError(t, err)
require.Equal(t,
nestedFieldRenderedTargets,
rT,
)
}
func TestNestedFieldDefaultsRender(t *testing.T) {
rT, err := RenderTemplate(nestedFieldRender, nil)
require.NoError(t, err)
require.Equal(t,
nestedFieldDefaultRenderedTargets,
rT,
)
}
var multiVarTemplate = QueryTemplate{
Title: "Test",
Variables: []TemplateVariable{
{
Key: "metricName",
},
{
Key: "anotherMetric",
},
},
Targets: []Target{
{
DataType: data.FrameTypeUnknown,
//DataTypeVersion: data.FrameTypeVersion{0, 0},
Variables: map[string][]VariableReplacement{
"metricName": {
{
Path: "$.expr",
Position: &Position{
Start: 4,
End: 14,
},
},
{
Path: "$.expr",
Position: &Position{
Start: 37,
End: 47,
},
},
},
"anotherMetric": {
{
Path: "$.expr",
Position: &Position{
Start: 21,
End: 34,
},
},
},
},
Properties: apidata.NewDataQuery(map[string]any{
"expr": "1 + metricName + 1 + anotherMetric + metricName",
}),
},
},
}
var multiVarRenderedTargets = []Target{
{
DataType: data.FrameTypeUnknown,
Variables: map[string][]VariableReplacement{
"metricName": {
{
Path: "$.expr",
Position: &Position{
Start: 4,
End: 14,
},
},
{
Path: "$.expr",
Position: &Position{
Start: 37,
End: 47,
},
},
},
"anotherMetric": {
{
Path: "$.expr",
Position: &Position{
Start: 21,
End: 34,
},
},
},
},
//DataTypeVersion: data.FrameTypeVersion{0, 0},
Properties: apidata.NewDataQuery(map[string]any{
"expr": "1 + up + 1 + sloths_do_like_a_good_nap + up",
}),
},
}
func TestMultiVarTemplate(t *testing.T) {
rT, err := RenderTemplate(multiVarTemplate, map[string][]string{
"metricName": {"up"},
"anotherMetric": {"sloths_do_like_a_good_nap"},
})
require.NoError(t, err)
require.Equal(t,
multiVarRenderedTargets,
rT,
)
}
func TestRenderWithRune(t *testing.T) {
qt := QueryTemplate{
Variables: []TemplateVariable{
{
Key: "name",
},
},
Targets: []Target{
{
Properties: apidata.NewDataQuery(map[string]any{
"message": "🐦 name!",
}),
Variables: map[string][]VariableReplacement{
"name": {
{
Path: "$.message",
Position: &Position{
Start: 2,
End: 6,
},
},
},
},
},
},
}
selectedValues := map[string][]string{
"name": {"🦥"},
}
rq, err := RenderTemplate(qt, selectedValues)
require.NoError(t, err)
require.Equal(t, "🐦 🦥!", rq[0].Properties.GetString("message"))
}
-123
View File
@@ -1,123 +0,0 @@
package template
import (
"github.com/grafana/grafana-plugin-sdk-go/data"
apidata "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1"
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
)
type QueryTemplate struct {
// A display name
Title string `json:"title,omitempty"`
// Longer description for why it is interesting
Description string `json:"description,omitempty"`
// Toggle for visible/hidden queries
IsVisible bool `json:"isVisible,omitempty"`
// The tags that can be used to filter the template
// +listType=set
Tags []string `json:"tags,omitempty"`
// Whether the query is locked and cannot be edited
// Note: This is purely for UI display purposes and not for security
IsLocked bool `json:"isLocked,omitempty"`
// The variables that can be used to render
// +listType=map
// +listMapKey=key
Variables []TemplateVariable `json:"vars,omitempty"`
// Output variables
// +listType=set
Targets []Target `json:"targets"`
}
type Target struct {
// DataType is the returned Dataplane type from the query.
DataType data.FrameType `json:"dataType,omitempty"`
// DataTypeVersion is the version for the Dataplane type.
// TODO 2[uint] seems to panic, maybe implement DeepCopy on data.FrameTypeVersion?
// DataTypeVersion data.FrameTypeVersion `json:"dataTypeVersion,omitempty"`
// Variables that will be replaced in the query
Variables map[string][]VariableReplacement `json:"variables"`
// Query target
Properties apidata.DataQuery `json:"properties"`
}
// TemplateVariable is the definition of a variable that will be interpolated
// in targets.
type TemplateVariable struct {
// Key is the name of the variable.
Key string `json:"key"`
// DefaultValue is the value to be used when there is no selected value
// during render.
// +listType=atomic
DefaultValues []string `json:"defaultValues,omitempty"`
// ValueListDefinition is the object definition used by the FE
// to get a list of possible values to select for render.
ValueListDefinition common.Unstructured `json:"valueListDefinition,omitempty"`
}
// QueryVariable is the definition of a variable that will be interpolated
// in targets.
type VariableReplacement struct {
// Path is the location of the property within a target.
// The format for this is not figured out yet (Maybe JSONPath?).
// Idea: ["string", int, "string"] where int indicates array offset
Path string `json:"path"`
// Positions is a list of where to perform the interpolation
// within targets during render.
// The first string is the Idx of the target as a string, since openAPI
// does not support ints as map keys
Position *Position `json:"position,omitempty"`
// How values should be interpolated
Format VariableFormat `json:"format,omitempty"`
}
// Define how to format values in the template.
// See: https://grafana.com/docs/grafana/latest/dashboards/variables/variable-syntax/#advanced-variable-format-options
// +enum
type VariableFormat string
// Defines values for ItemType.
const (
// Formats variables with multiple values as a comma-separated string.
FormatCSV VariableFormat = "csv"
// Formats variables with multiple values as a comma-separated string.
FormatJSON VariableFormat = "json"
// Formats single- and multi-valued variables into a comma-separated string
FormatDoubleQuote VariableFormat = "doublequote"
// Formats single- and multi-valued variables into a comma-separated string
FormatSingleQuote VariableFormat = "singlequote"
// Formats variables with multiple values into a pipe-separated string.
FormatPipe VariableFormat = "pipe"
// Formats variables with multiple values into comma-separated string.
// This is the default behavior when no format is specified
FormatRaw VariableFormat = "raw"
)
// Position is where to do replacement in the targets
// during render.
type Position struct {
// Start is the byte offset within TargetKey's property of the variable.
// It is the start location for replacements).
Start int64 `json:"start"` // TODO: byte, rune?
// End is the byte offset of the end of the variable.
End int64 `json:"end"`
}
@@ -1,136 +0,0 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
// SPDX-License-Identifier: AGPL-3.0-only
// Code generated by deepcopy-gen. DO NOT EDIT.
package template
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Position) DeepCopyInto(out *Position) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Position.
func (in *Position) DeepCopy() *Position {
if in == nil {
return nil
}
out := new(Position)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *QueryTemplate) DeepCopyInto(out *QueryTemplate) {
*out = *in
if in.Tags != nil {
in, out := &in.Tags, &out.Tags
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.Variables != nil {
in, out := &in.Variables, &out.Variables
*out = make([]TemplateVariable, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.Targets != nil {
in, out := &in.Targets, &out.Targets
*out = make([]Target, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new QueryTemplate.
func (in *QueryTemplate) DeepCopy() *QueryTemplate {
if in == nil {
return nil
}
out := new(QueryTemplate)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Target) DeepCopyInto(out *Target) {
*out = *in
if in.Variables != nil {
in, out := &in.Variables, &out.Variables
*out = make(map[string][]VariableReplacement, len(*in))
for key, val := range *in {
var outVal []VariableReplacement
if val == nil {
(*out)[key] = nil
} else {
in, out := &val, &outVal
*out = make([]VariableReplacement, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
(*out)[key] = outVal
}
}
in.Properties.DeepCopyInto(&out.Properties)
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Target.
func (in *Target) DeepCopy() *Target {
if in == nil {
return nil
}
out := new(Target)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TemplateVariable) DeepCopyInto(out *TemplateVariable) {
*out = *in
if in.DefaultValues != nil {
in, out := &in.DefaultValues, &out.DefaultValues
*out = make([]string, len(*in))
copy(*out, *in)
}
in.ValueListDefinition.DeepCopyInto(&out.ValueListDefinition)
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TemplateVariable.
func (in *TemplateVariable) DeepCopy() *TemplateVariable {
if in == nil {
return nil
}
out := new(TemplateVariable)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *VariableReplacement) DeepCopyInto(out *VariableReplacement) {
*out = *in
if in.Position != nil {
in, out := &in.Position, &out.Position
*out = new(Position)
**out = **in
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VariableReplacement.
func (in *VariableReplacement) DeepCopy() *VariableReplacement {
if in == nil {
return nil
}
out := new(VariableReplacement)
in.DeepCopyInto(out)
return out
}
@@ -1,19 +0,0 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
// SPDX-License-Identifier: AGPL-3.0-only
// Code generated by defaulter-gen. DO NOT EDIT.
package template
import (
runtime "k8s.io/apimachinery/pkg/runtime"
)
// RegisterDefaults adds defaulters functions to the given scheme.
// Public to allow building arbitrary schemes.
// All generated defaulters are covering - they call all nested defaulters.
func RegisterDefaults(scheme *runtime.Scheme) error {
return nil
}
+6 -318
View File
@@ -14,18 +14,12 @@ import (
func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
return map[string]common.OpenAPIDefinition{
"github.com/grafana/grafana/pkg/apis/query/v0alpha1.DataSourceApiServer": schema_pkg_apis_query_v0alpha1_DataSourceApiServer(ref),
"github.com/grafana/grafana/pkg/apis/query/v0alpha1.DataSourceApiServerList": schema_pkg_apis_query_v0alpha1_DataSourceApiServerList(ref),
"github.com/grafana/grafana/pkg/apis/query/v0alpha1.QueryDataRequest": schema_pkg_apis_query_v0alpha1_QueryDataRequest(ref),
"github.com/grafana/grafana/pkg/apis/query/v0alpha1.QueryDataResponse": schema_pkg_apis_query_v0alpha1_QueryDataResponse(ref),
"github.com/grafana/grafana/pkg/apis/query/v0alpha1.QueryTypeDefinition": schema_pkg_apis_query_v0alpha1_QueryTypeDefinition(ref),
"github.com/grafana/grafana/pkg/apis/query/v0alpha1.QueryTypeDefinitionList": schema_pkg_apis_query_v0alpha1_QueryTypeDefinitionList(ref),
"github.com/grafana/grafana/pkg/apis/query/v0alpha1/template.Position": schema_apis_query_v0alpha1_template_Position(ref),
"github.com/grafana/grafana/pkg/apis/query/v0alpha1/template.QueryTemplate": schema_apis_query_v0alpha1_template_QueryTemplate(ref),
"github.com/grafana/grafana/pkg/apis/query/v0alpha1/template.Target": schema_apis_query_v0alpha1_template_Target(ref),
"github.com/grafana/grafana/pkg/apis/query/v0alpha1/template.TemplateVariable": schema_apis_query_v0alpha1_template_TemplateVariable(ref),
"github.com/grafana/grafana/pkg/apis/query/v0alpha1/template.VariableReplacement": schema_apis_query_v0alpha1_template_VariableReplacement(ref),
"github.com/grafana/grafana/pkg/apis/query/v0alpha1/template.replacement": schema_apis_query_v0alpha1_template_replacement(ref),
"github.com/grafana/grafana/pkg/apis/query/v0alpha1.DataSourceApiServer": schema_pkg_apis_query_v0alpha1_DataSourceApiServer(ref),
"github.com/grafana/grafana/pkg/apis/query/v0alpha1.DataSourceApiServerList": schema_pkg_apis_query_v0alpha1_DataSourceApiServerList(ref),
"github.com/grafana/grafana/pkg/apis/query/v0alpha1.QueryDataRequest": schema_pkg_apis_query_v0alpha1_QueryDataRequest(ref),
"github.com/grafana/grafana/pkg/apis/query/v0alpha1.QueryDataResponse": schema_pkg_apis_query_v0alpha1_QueryDataResponse(ref),
"github.com/grafana/grafana/pkg/apis/query/v0alpha1.QueryTypeDefinition": schema_pkg_apis_query_v0alpha1_QueryTypeDefinition(ref),
"github.com/grafana/grafana/pkg/apis/query/v0alpha1.QueryTypeDefinitionList": schema_pkg_apis_query_v0alpha1_QueryTypeDefinitionList(ref),
}
}
@@ -351,309 +345,3 @@ func schema_pkg_apis_query_v0alpha1_QueryTypeDefinitionList(ref common.Reference
"github.com/grafana/grafana/pkg/apis/query/v0alpha1.QueryTypeDefinition", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
}
}
func schema_apis_query_v0alpha1_template_Position(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "Position is where to do replacement in the targets during render.",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"start": {
SchemaProps: spec.SchemaProps{
Description: "Start is the byte offset within TargetKey's property of the variable. It is the start location for replacements).",
Default: 0,
Type: []string{"integer"},
Format: "int64",
},
},
"end": {
SchemaProps: spec.SchemaProps{
Description: "End is the byte offset of the end of the variable.",
Default: 0,
Type: []string{"integer"},
Format: "int64",
},
},
},
Required: []string{"start", "end"},
},
},
}
}
func schema_apis_query_v0alpha1_template_QueryTemplate(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"title": {
SchemaProps: spec.SchemaProps{
Description: "A display name",
Type: []string{"string"},
Format: "",
},
},
"description": {
SchemaProps: spec.SchemaProps{
Description: "Longer description for why it is interesting",
Type: []string{"string"},
Format: "",
},
},
"isVisible": {
SchemaProps: spec.SchemaProps{
Description: "Toggle for visible/hidden queries",
Type: []string{"boolean"},
Format: "",
},
},
"tags": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-list-type": "set",
},
},
SchemaProps: spec.SchemaProps{
Description: "The tags that can be used to filter the template",
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: "",
Type: []string{"string"},
Format: "",
},
},
},
},
},
"isLocked": {
SchemaProps: spec.SchemaProps{
Description: "Whether the query is locked and cannot be edited Note: This is purely for UI display purposes and not for security",
Type: []string{"boolean"},
Format: "",
},
},
"vars": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-list-map-keys": []interface{}{
"key",
},
"x-kubernetes-list-type": "map",
},
},
SchemaProps: spec.SchemaProps{
Description: "The variables that can be used to render",
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/pkg/apis/query/v0alpha1/template.TemplateVariable"),
},
},
},
},
},
"targets": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-list-type": "set",
},
},
SchemaProps: spec.SchemaProps{
Description: "Output variables",
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/pkg/apis/query/v0alpha1/template.Target"),
},
},
},
},
},
},
Required: []string{"targets"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apis/query/v0alpha1/template.Target", "github.com/grafana/grafana/pkg/apis/query/v0alpha1/template.TemplateVariable"},
}
}
func schema_apis_query_v0alpha1_template_Target(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"dataType": {
SchemaProps: spec.SchemaProps{
Description: "DataType is the returned Dataplane type from the query.",
Type: []string{"string"},
Format: "",
},
},
"variables": {
SchemaProps: spec.SchemaProps{
Description: "Variables that will be replaced in the query",
Type: []string{"object"},
AdditionalProperties: &spec.SchemaOrBool{
Allows: true,
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/pkg/apis/query/v0alpha1/template.VariableReplacement"),
},
},
},
},
},
},
},
},
"properties": {
SchemaProps: spec.SchemaProps{
Description: "Query target",
Ref: ref("github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1.DataQuery"),
},
},
},
Required: []string{"variables", "properties"},
},
},
Dependencies: []string{
"github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1.DataQuery", "github.com/grafana/grafana/pkg/apis/query/v0alpha1/template.VariableReplacement"},
}
}
func schema_apis_query_v0alpha1_template_TemplateVariable(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "TemplateVariable is the definition of a variable that will be interpolated in targets.",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"key": {
SchemaProps: spec.SchemaProps{
Description: "Key is the name of the variable.",
Default: "",
Type: []string{"string"},
Format: "",
},
},
"defaultValues": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-list-type": "atomic",
},
},
SchemaProps: spec.SchemaProps{
Description: "DefaultValue is the value to be used when there is no selected value during render.",
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: "",
Type: []string{"string"},
Format: "",
},
},
},
},
},
"valueListDefinition": {
SchemaProps: spec.SchemaProps{
Description: "ValueListDefinition is the object definition used by the FE to get a list of possible values to select for render.",
Ref: ref("github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured"),
},
},
},
Required: []string{"key"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured"},
}
}
func schema_apis_query_v0alpha1_template_VariableReplacement(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "QueryVariable is the definition of a variable that will be interpolated in targets.",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"path": {
SchemaProps: spec.SchemaProps{
Description: "Path is the location of the property within a target. The format for this is not figured out yet (Maybe JSONPath?). Idea: [\"string\", int, \"string\"] where int indicates array offset",
Default: "",
Type: []string{"string"},
Format: "",
},
},
"position": {
SchemaProps: spec.SchemaProps{
Description: "Positions is a list of where to perform the interpolation within targets during render. The first string is the Idx of the target as a string, since openAPI does not support ints as map keys",
Ref: ref("github.com/grafana/grafana/pkg/apis/query/v0alpha1/template.Position"),
},
},
"format": {
SchemaProps: spec.SchemaProps{
Description: "How values should be interpolated\n\nPossible enum values:\n - `\"csv\"` Formats variables with multiple values as a comma-separated string.\n - `\"doublequote\"` Formats single- and multi-valued variables into a comma-separated string\n - `\"json\"` Formats variables with multiple values as a comma-separated string.\n - `\"pipe\"` Formats variables with multiple values into a pipe-separated string.\n - `\"raw\"` Formats variables with multiple values into comma-separated string. This is the default behavior when no format is specified\n - `\"singlequote\"` Formats single- and multi-valued variables into a comma-separated string",
Type: []string{"string"},
Format: "",
Enum: []interface{}{"csv", "doublequote", "json", "pipe", "raw", "singlequote"},
},
},
},
Required: []string{"path"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apis/query/v0alpha1/template.Position"},
}
}
func schema_apis_query_v0alpha1_template_replacement(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"Position": {
SchemaProps: spec.SchemaProps{
Ref: ref("github.com/grafana/grafana/pkg/apis/query/v0alpha1/template.Position"),
},
},
"TemplateVariable": {
SchemaProps: spec.SchemaProps{
Ref: ref("github.com/grafana/grafana/pkg/apis/query/v0alpha1/template.TemplateVariable"),
},
},
"format": {
SchemaProps: spec.SchemaProps{
Description: "Possible enum values:\n - `\"csv\"` Formats variables with multiple values as a comma-separated string.\n - `\"doublequote\"` Formats single- and multi-valued variables into a comma-separated string\n - `\"json\"` Formats variables with multiple values as a comma-separated string.\n - `\"pipe\"` Formats variables with multiple values into a pipe-separated string.\n - `\"raw\"` Formats variables with multiple values into comma-separated string. This is the default behavior when no format is specified\n - `\"singlequote\"` Formats single- and multi-valued variables into a comma-separated string",
Default: "",
Type: []string{"string"},
Format: "",
Enum: []interface{}{"csv", "doublequote", "json", "pipe", "raw", "singlequote"},
},
},
},
Required: []string{"Position", "TemplateVariable", "format"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apis/query/v0alpha1/template.Position", "github.com/grafana/grafana/pkg/apis/query/v0alpha1/template.TemplateVariable"},
}
}
@@ -1,5 +1 @@
API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/query/v0alpha1,DataSourceApiServer,AliasIDs
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/query/v0alpha1/template,QueryTemplate,Variables
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/query/v0alpha1/template,replacement,Position
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/query/v0alpha1/template,replacement,TemplateVariable
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/query/v0alpha1/template,replacement,format
+1 -2
View File
@@ -64,13 +64,12 @@ require (
go.etcd.io/etcd/client/v3 v3.5.21 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/otel v1.36.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 // indirect
go.opentelemetry.io/otel/metric v1.36.0 // indirect
go.opentelemetry.io/otel/sdk v1.36.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.36.0 // indirect
go.opentelemetry.io/otel/trace v1.36.0 // indirect
go.opentelemetry.io/proto/otlp v1.6.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
+2 -2
View File
@@ -188,8 +188,8 @@ go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJyS
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 h1:dNzwXjZKpMpE2JhmO+9HsPl42NIXFIFSUSSs0fiqra0=
+2 -2
View File
@@ -9,7 +9,7 @@ require (
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e
github.com/prometheus/client_golang v1.22.0
github.com/stretchr/testify v1.10.0
go.opentelemetry.io/contrib/propagators/jaeger v1.35.0
go.opentelemetry.io/contrib/propagators/jaeger v1.36.0
go.opentelemetry.io/otel v1.36.0
go.opentelemetry.io/otel/trace v1.36.0
k8s.io/apimachinery v0.33.1
@@ -73,7 +73,7 @@ require (
go.etcd.io/etcd/client/v3 v3.5.21 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 // indirect
go.opentelemetry.io/otel/metric v1.36.0 // indirect
+4 -4
View File
@@ -201,10 +201,10 @@ go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJyS
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
go.opentelemetry.io/contrib/propagators/jaeger v1.35.0 h1:UIrZgRBHUrYRlJ4V419lVb4rs2ar0wFzKNAebaP05XU=
go.opentelemetry.io/contrib/propagators/jaeger v1.35.0/go.mod h1:0ciyFyYZxE6JqRAQvIgGRabKWDUmNdW3GAQb6y/RlFU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/contrib/propagators/jaeger v1.36.0 h1:SoCgXYF4ISDtNyfLUzsGDaaudZVTx2yJhOyBO0+/GYk=
go.opentelemetry.io/contrib/propagators/jaeger v1.36.0/go.mod h1:VHu48l0YTRKSObdPQ+Sb8xMZvdnJlN7yhHuHoPgNqHM=
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 h1:dNzwXjZKpMpE2JhmO+9HsPl42NIXFIFSUSSs0fiqra0=
+34 -40
View File
@@ -102,6 +102,7 @@ func SetDualWritingMode(
ctx context.Context,
kvs NamespacedKVStore,
cfg *SyncerConfig,
metrics *DualWriterMetrics,
) (DualWriterMode, error) {
if cfg == nil {
return Mode0, errors.New("syncer config is nil")
@@ -123,17 +124,17 @@ func SetDualWritingMode(
errDualWriterSetCurrentMode := errors.New("failed to set current dual writing mode")
// Use entity name as key
m, ok, err := kvs.Get(ctx, cfg.Kind)
kvMode, ok, err := kvs.Get(ctx, cfg.Kind)
if err != nil {
return Mode0, errors.New("failed to fetch current dual writing mode")
}
currentMode, exists := toMode[m]
currentMode, exists := toMode[kvMode]
// If the mode does not exist in our mapping, we log an error.
if !exists && ok {
// Only log if "ok" because initially all instances will have mode unset for playlists.
klog.Infof("invalid dual writing mode for %s mode: %v", cfg.Kind, m)
klog.Infof("invalid dual writing mode for %s mode: %v", cfg.Kind, kvMode)
}
// If the mode does not exist in our mapping, and we also didn't find an entry for this kind, fallback.
@@ -145,51 +146,44 @@ func SetDualWritingMode(
}
}
// Handle transitions to the desired mode.
switch {
case cfg.Mode == Mode2 || cfg.Mode == Mode1:
// Directly set the mode for Mode1 and Mode2.
currentMode = cfg.Mode
if err := kvs.Set(ctx, cfg.Kind, fmt.Sprint(currentMode)); err != nil {
return Mode0, errDualWriterSetCurrentMode
}
case cfg.Mode >= Mode3 && currentMode < Mode3:
if cfg.SkipDataSync {
return currentMode, nil
}
// Transitioning to Mode3 or higher requires data synchronization.
cfgModeTmp := cfg.Mode
// Before running the sync, set the syncer config to the current mode, as we have to run the syncer
// once in the current active mode before we can upgrade.
cfg.Mode = currentMode
syncOk, err := runDataSyncer(ctx, cfg)
// Once we are done with running the syncer, we can change the mode back on the config to the desired one.
cfg.Mode = cfgModeTmp
if err != nil {
klog.Error("data syncer failed for mode:", m, "err", err)
return currentMode, nil
}
if !syncOk {
klog.Info("data syncer not ok for mode:", m)
return currentMode, nil
}
// If sync is successful, update the mode to the desired one.
isUpgradeToReadUnifiedMode := currentMode < Mode3 && cfg.Mode >= Mode3
if !isUpgradeToReadUnifiedMode {
if err := kvs.Set(ctx, cfg.Kind, fmt.Sprint(cfg.Mode)); err != nil {
return Mode0, errDualWriterSetCurrentMode
}
return cfg.Mode, nil
case cfg.Mode >= Mode3 && currentMode >= Mode3:
// If already in Mode3 or higher, simply update to the desired mode.
currentMode = cfg.Mode
if err := kvs.Set(ctx, cfg.Kind, fmt.Sprint(currentMode)); err != nil {
}
// If SkipDataSync is enabled, we can set the mode directly without running the syncer.
if cfg.SkipDataSync {
if err := kvs.Set(ctx, cfg.Kind, fmt.Sprint(cfg.Mode)); err != nil {
return Mode0, errDualWriterSetCurrentMode
}
default:
// Handle any unexpected cases (should not normally happen).
return cfg.Mode, nil
}
// Transitioning to Mode3 or higher from Mode0, Mode1, or Mode2.
// We need to run the syncer in the current mode before we can upgrade to Mode3 or higher.
cfgModeTmp := cfg.Mode
// Before running the sync, set the syncer config to the current mode, as we have to run the syncer
// once in the current active mode before we can upgrade.
cfg.Mode = currentMode
syncOk, err := runDataSyncer(ctx, cfg, metrics)
// Once we are done with running the syncer, we can change the mode back on the config to the desired one.
cfg.Mode = cfgModeTmp
if err != nil {
klog.Error("data syncer failed for mode:", kvMode, "err", err)
return currentMode, nil
}
if !syncOk {
klog.Info("data syncer not ok for mode:", kvMode)
return currentMode, nil
}
// If sync is successful, update the mode to the desired one.
if err := kvs.Set(ctx, cfg.Kind, fmt.Sprint(cfg.Mode)); err != nil {
return Mode0, errDualWriterSetCurrentMode
}
return currentMode, nil
return cfg.Mode, nil
}
var defaultConverter = runtime.UnstructuredConverter(runtime.DefaultUnstructuredConverter)
+6 -14
View File
@@ -6,7 +6,6 @@ import (
"math/rand"
"time"
"github.com/prometheus/client_golang/prometheus"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
@@ -40,8 +39,6 @@ type SyncerConfig struct {
DataSyncerInterval time.Duration
DataSyncerRecordsLimit int
Reg prometheus.Registerer
}
func (s *SyncerConfig) Validate() error {
@@ -69,15 +66,12 @@ func (s *SyncerConfig) Validate() error {
if s.DataSyncerRecordsLimit == 0 {
s.DataSyncerRecordsLimit = 1000
}
if s.Reg == nil {
s.Reg = prometheus.DefaultRegisterer
}
return nil
}
// StartPeriodicDataSyncer starts a background job that will execute the DataSyncer, syncing the data
// from the hosted grafana backend into the unified storage backend. This is run in the grafana instance.
func StartPeriodicDataSyncer(ctx context.Context, cfg *SyncerConfig) error {
func StartPeriodicDataSyncer(ctx context.Context, cfg *SyncerConfig, metrics *DualWriterMetrics) error {
if err := cfg.Validate(); err != nil {
return fmt.Errorf("invalid syncer config: %w", err)
}
@@ -95,14 +89,14 @@ func StartPeriodicDataSyncer(ctx context.Context, cfg *SyncerConfig) error {
time.Sleep(time.Second * time.Duration(jitterSeconds))
// run it immediately
syncOK, err := runDataSyncer(ctx, cfg)
syncOK, err := runDataSyncer(ctx, cfg, metrics)
log.Info("data syncer finished", "syncOK", syncOK, "error", err)
ticker := time.NewTicker(cfg.DataSyncerInterval)
for {
select {
case <-ticker.C:
syncOK, err = runDataSyncer(ctx, cfg)
syncOK, err = runDataSyncer(ctx, cfg, metrics)
log.Info("data syncer finished", "syncOK", syncOK, ", error", err)
case <-ctx.Done():
return
@@ -114,7 +108,7 @@ func StartPeriodicDataSyncer(ctx context.Context, cfg *SyncerConfig) error {
// runDataSyncer will ensure that data between legacy storage and unified storage are in sync.
// The sync implementation depends on the DualWriter mode
func runDataSyncer(ctx context.Context, cfg *SyncerConfig) (bool, error) {
func runDataSyncer(ctx context.Context, cfg *SyncerConfig, metrics *DualWriterMetrics) (bool, error) {
if err := cfg.Validate(); err != nil {
return false, fmt.Errorf("invalid syncer config: %w", err)
}
@@ -126,19 +120,17 @@ func runDataSyncer(ctx context.Context, cfg *SyncerConfig) (bool, error) {
// implementation depends on the current DualWriter mode
switch cfg.Mode {
case Mode1, Mode2:
return legacyToUnifiedStorageDataSyncer(ctx, cfg)
return legacyToUnifiedStorageDataSyncer(ctx, cfg, metrics)
default:
klog.Info("data syncer not implemented for mode:", cfg.Mode)
return false, nil
}
}
func legacyToUnifiedStorageDataSyncer(ctx context.Context, cfg *SyncerConfig) (bool, error) {
func legacyToUnifiedStorageDataSyncer(ctx context.Context, cfg *SyncerConfig, metrics *DualWriterMetrics) (bool, error) {
if err := cfg.Validate(); err != nil {
return false, fmt.Errorf("invalid syncer config: %w", err)
}
metrics := &dualWriterMetrics{}
metrics.init(cfg.Reg)
log := klog.NewKlogr().WithName("legacyToUnifiedStorageDataSyncer").WithValues("mode", cfg.Mode, "resource", cfg.Kind)
+2 -4
View File
@@ -201,13 +201,12 @@ func TestLegacyToUnifiedStorage_DataSyncer(t *testing.T) {
LegacyStorage: ls,
Storage: us,
Kind: "test.kind",
Reg: p,
ServerLockService: &fakeServerLock{},
RequestInfo: &request.RequestInfo{},
DataSyncerRecordsLimit: 1000,
DataSyncerInterval: time.Hour,
})
}, NewDualWriterMetrics(nil))
if tt.wantErr {
assert.Error(t, err)
return
@@ -241,13 +240,12 @@ func TestLegacyToUnifiedStorage_DataSyncer(t *testing.T) {
LegacyStorage: ls,
Storage: us,
Kind: "test.kind",
Reg: p,
ServerLockService: &fakeServerLock{},
RequestInfo: &request.RequestInfo{},
DataSyncerRecordsLimit: 1000,
DataSyncerInterval: time.Hour,
})
}, NewDualWriterMetrics(nil))
if tt.wantErr {
assert.Error(t, err)
return
+39 -28
View File
@@ -20,54 +20,62 @@ func TestSetDualWritingMode(t *testing.T) {
kvStore *fakeNamespacedKV
desiredMode DualWriterMode
expectedMode DualWriterMode
expectedKVMode string
skipDataSync bool
serverLockError error
}
tests :=
[]testCase{
{
name: "should return a mode 2 dual writer when mode 2 is set as the desired mode",
kvStore: &fakeNamespacedKV{data: map[string]string{"playlist.grafana.app/playlists": "2"}, namespace: "storage.dualwriting"},
desiredMode: Mode2,
expectedMode: Mode2,
name: "should return a mode 2 dual writer when mode 2 is set as the desired mode",
kvStore: &fakeNamespacedKV{data: map[string]string{"playlist.grafana.app/playlists": "2"}, namespace: "storage.dualwriting"},
desiredMode: Mode2,
expectedMode: Mode2,
expectedKVMode: "2",
},
{
name: "should return a mode 1 dual writer when mode 1 is set as the desired mode",
kvStore: &fakeNamespacedKV{data: map[string]string{"playlist.grafana.app/playlists": "2"}, namespace: "storage.dualwriting"},
desiredMode: Mode1,
expectedMode: Mode1,
name: "should return a mode 1 dual writer when mode 1 is set as the desired mode",
kvStore: &fakeNamespacedKV{data: map[string]string{"playlist.grafana.app/playlists": "2"}, namespace: "storage.dualwriting"},
desiredMode: Mode1,
expectedMode: Mode1,
expectedKVMode: "1",
},
{
name: "should return mode 3 as desired mode when current mode is > 3",
kvStore: &fakeNamespacedKV{data: map[string]string{"playlist.grafana.app/playlists": "5"}, namespace: "storage.dualwriting"},
desiredMode: Mode3,
expectedMode: Mode3,
name: "should return mode 3 as desired mode when current mode is > 3",
kvStore: &fakeNamespacedKV{data: map[string]string{"playlist.grafana.app/playlists": "5"}, namespace: "storage.dualwriting"},
desiredMode: Mode3,
expectedMode: Mode3,
expectedKVMode: "3",
},
{
name: "should return mode 3 as desired mode when current mode is 2",
kvStore: &fakeNamespacedKV{data: map[string]string{"playlist.grafana.app/playlists": "2"}, namespace: "storage.dualwriting"},
desiredMode: Mode3,
expectedMode: Mode3,
name: "should return mode 3 as desired mode when current mode is 2",
kvStore: &fakeNamespacedKV{data: map[string]string{"playlist.grafana.app/playlists": "2"}, namespace: "storage.dualwriting"},
desiredMode: Mode3,
expectedMode: Mode3,
expectedKVMode: "3",
},
{
name: "should default to mode 0 if there is no desired mode",
kvStore: &fakeNamespacedKV{data: map[string]string{}, namespace: "storage.dualwriting"},
desiredMode: Mode0,
expectedMode: Mode0,
name: "should default to mode 0 if there is no desired mode",
kvStore: &fakeNamespacedKV{data: map[string]string{}, namespace: "storage.dualwriting"},
desiredMode: Mode0,
expectedMode: Mode0,
expectedKVMode: "",
},
{
name: "should keep mode2 when trying to go from mode2 to mode3 and the server lock service returns an error",
kvStore: &fakeNamespacedKV{data: map[string]string{"playlist.grafana.app/playlists": "2"}, namespace: "storage.dualwriting"},
desiredMode: Mode3,
expectedMode: Mode2,
expectedKVMode: "2",
serverLockError: fmt.Errorf("lock already exists"),
},
{
name: "should keep mode2 when trying to go from mode2 to mode3 and migration is disabled",
kvStore: &fakeNamespacedKV{data: map[string]string{"playlist.grafana.app/playlists": "2"}, namespace: "storage.dualwriting"},
desiredMode: Mode3,
expectedMode: Mode2,
skipDataSync: true,
name: "should keep mode2 when trying to go from mode2 to mode3 and migration is disabled",
kvStore: &fakeNamespacedKV{data: map[string]string{"playlist.grafana.app/playlists": "2"}, namespace: "storage.dualwriting"},
desiredMode: Mode3,
expectedMode: Mode3,
expectedKVMode: "3",
skipDataSync: true,
},
}
@@ -97,13 +105,16 @@ func TestSetDualWritingMode(t *testing.T) {
SkipDataSync: tt.skipDataSync,
ServerLockService: serverLockSvc,
RequestInfo: &request.RequestInfo{},
Reg: p,
DataSyncerRecordsLimit: 1000,
DataSyncerInterval: time.Hour,
})
}, NewDualWriterMetrics(nil))
require.NoError(t, err)
require.Equal(t, tt.expectedMode, dwMode)
kvMode, _, err := tt.kvStore.Get(context.Background(), "playlist.grafana.app/playlists")
require.NoError(t, err)
require.Equal(t, tt.expectedKVMode, kvMode, "expected mode for playlist.grafana.app/playlists")
}
}
@@ -162,7 +173,7 @@ func (f *fakeNamespacedKV) Get(ctx context.Context, key string) (string, bool, e
}
func (f *fakeNamespacedKV) Set(ctx context.Context, key, value string) error {
f.data[f.namespace+key] = value
f.data[key] = value
return nil
}
+21 -28
View File
@@ -6,47 +6,40 @@ import (
"time"
"github.com/prometheus/client_golang/prometheus"
"k8s.io/klog/v2"
"github.com/prometheus/client_golang/prometheus/promauto"
)
type dualWriterMetrics struct {
syncer *prometheus.HistogramVec
type DualWriterMetrics struct {
// DualWriterSyncerDuration is a metric summary for dual writer sync duration per mode
syncer *prometheus.HistogramVec
// DualWriterDataSyncerOutcome is a metric summary for dual writer data syncer outcome comparison between the 2 stores per mode
syncerOutcome *prometheus.HistogramVec
}
// DualWriterSyncerDuration is a metric summary for dual writer sync duration per mode
var DualWriterSyncerDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Name: "dual_writer_data_syncer_duration_seconds",
Help: "Histogram for the runtime of dual writer data syncer duration per mode",
Namespace: "grafana",
NativeHistogramBucketFactor: 1.1,
}, []string{"is_error", "mode", "resource"})
func NewDualWriterMetrics(reg prometheus.Registerer) *DualWriterMetrics {
return &DualWriterMetrics{
syncer: promauto.With(reg).NewHistogramVec(prometheus.HistogramOpts{
Name: "dual_writer_data_syncer_duration_seconds",
Help: "Histogram for the runtime of dual writer data syncer duration per mode",
Namespace: "grafana",
NativeHistogramBucketFactor: 1.1,
}, []string{"is_error", "mode", "resource"}),
// DualWriterDataSyncerOutcome is a metric summary for dual writer data syncer outcome comparison between the 2 stores per mode
var DualWriterDataSyncerOutcome = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Name: "dual_writer_data_syncer_outcome",
Help: "Histogram for the runtime of dual writer data syncer outcome comparison between the 2 stores per mode",
Namespace: "grafana",
NativeHistogramBucketFactor: 1.1,
}, []string{"mode", "resource"})
func (m *dualWriterMetrics) init(reg prometheus.Registerer) {
log := klog.NewKlogr()
m.syncer = DualWriterSyncerDuration
m.syncerOutcome = DualWriterDataSyncerOutcome
errSyncer := reg.Register(m.syncer)
errSyncerOutcome := reg.Register(m.syncerOutcome)
if errSyncer != nil || errSyncerOutcome != nil {
log.Info("cloud migration metrics already registered")
syncerOutcome: promauto.With(reg).NewHistogramVec(prometheus.HistogramOpts{
Name: "dual_writer_data_syncer_outcome",
Help: "Histogram for the runtime of dual writer data syncer outcome comparison between the 2 stores per mode",
Namespace: "grafana",
NativeHistogramBucketFactor: 1.1,
}, []string{"mode", "resource"}),
}
}
func (m *dualWriterMetrics) recordDataSyncerDuration(isError bool, mode DualWriterMode, resource string, startFrom time.Time) {
func (m *DualWriterMetrics) recordDataSyncerDuration(isError bool, mode DualWriterMode, resource string, startFrom time.Time) {
duration := time.Since(startFrom).Seconds()
m.syncer.WithLabelValues(strconv.FormatBool(isError), fmt.Sprintf("%d", mode), resource).Observe(duration)
}
func (m *dualWriterMetrics) recordDataSyncerOutcome(mode DualWriterMode, resource string, synced bool) {
func (m *DualWriterMetrics) recordDataSyncerOutcome(mode DualWriterMode, resource string, synced bool) {
var observeValue float64
if !synced {
observeValue = 1
-3
View File
@@ -5,7 +5,6 @@ import (
"errors"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/mock"
metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -21,8 +20,6 @@ var anotherObj = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta
var exampleList = &example.PodList{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ListMeta: metav1.ListMeta{}, Items: []example.Pod{*exampleObj}}
var anotherList = &example.PodList{Items: []example.Pod{*anotherObj}}
var p = prometheus.NewRegistry()
type storageMock struct {
*mock.Mock
Storage
+22
View File
@@ -0,0 +1,22 @@
# Pa11y accessability tests
We use pa11y to run some automated simple accessability tests. They're ran with dagger to help orchestrate starting server + tests in a reproducable manner.
To run the tests locally:
1. Install dagger locally https://docs.dagger.io/install/
2. Grab the grafana.tar.gz artifact by either
1. Downloading it from the Github Action artifact from your PR
1. Build it locally with:
```sh
dagger run go run ./pkg/build/cmd artifacts -a targz:grafana:linux/amd64 --grafana-dir="$PWD" > dist/files.txt
cat dist/files.txt # Will output the path to the grafana.tar.gz
```
3. Run the dagger pipeline with:
```sh
dagger -v run go run ./pkg/build/a11y --package=(full path to .tar.gz) --results=./pa11y-ci-results.json
```
The JSON results file will be saved to the file from the `--results` arg
4. If they fail and you want to see the full output
1. Run the dagger command with `dagger -vE [...]`
2. At the end, arrow up to the exec pa11y-ci segment and hit Enter
+53 -26
View File
@@ -6,12 +6,16 @@ import (
"log"
"os"
"os/signal"
"path/filepath"
"dagger.io/dagger"
"github.com/urfave/cli/v3"
)
var (
grafanaHost = "grafana"
grafanaPort = 3001
)
func main() {
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()
@@ -49,8 +53,21 @@ func NewApp() *cli.Command {
TakesFile: true,
},
&cli.StringFlag{
Name: "flags",
Usage: "Flags to pass through to the e2e runner",
Name: "config",
Usage: "Path to the pa11y config file to use",
Value: "e2e/pa11yci.conf.js",
Validator: mustBeFile("config", true),
TakesFile: true,
},
&cli.StringFlag{
Name: "results",
Usage: "Path to the pa11y results file to export",
TakesFile: true,
},
&cli.BoolFlag{
Name: "no-threshold-fail",
Usage: "Don't fail the task if any of the tests fail. Use this in combination with --results to list all violations even if they're within thresholds",
Value: false,
},
},
Action: run,
@@ -61,26 +78,29 @@ func run(ctx context.Context, cmd *cli.Command) error {
grafanaDir := cmd.String("grafana-dir")
targzPath := cmd.String("package")
licensePath := cmd.String("license")
runnerFlags := cmd.String("flags")
pa11yConfigPath := cmd.String("config")
pa11yResultsPath := cmd.String("results")
noThresholdFail := cmd.Bool("no-threshold-fail")
d, err := dagger.Connect(ctx)
if err != nil {
return fmt.Errorf("failed to connect to Dagger: %w", err)
}
yarnCache := d.CacheVolume("yarn")
//nolint:gosec
nvmrcContents, err := os.ReadFile(filepath.Join(grafanaDir, ".nvmrc"))
if err != nil {
return fmt.Errorf("failed to read .nvmrc file: %w", err)
}
nodeVersion := string(nvmrcContents)
grafana := d.Host().Directory(grafanaDir, dagger.HostDirectoryOpts{
Exclude: []string{"node_modules", "*.tar.gz"},
// Explicitly only the files used by the grafana-server service
hostSrc := d.Host().Directory(grafanaDir, dagger.HostDirectoryOpts{
Include: []string{
"./devenv",
"./e2e/test-plugins", // Directory is included so provisioning works, but they're not actually build
"./scripts/grafana-server/custom.ini",
"./scripts/grafana-server/start-server",
"./scripts/grafana-server/kill-server",
"./scripts/grafana-server/variables",
},
})
targz := d.Host().File(targzPath)
pa11yConfig := d.Host().File(pa11yConfigPath)
var license *dagger.File
if licensePath != "" {
@@ -88,27 +108,34 @@ func run(ctx context.Context, cmd *cli.Command) error {
}
svc, err := GrafanaService(ctx, d, GrafanaServiceOpts{
GrafanaDir: grafana,
HostSrc: hostSrc,
GrafanaTarGz: targz,
License: license,
YarnCache: yarnCache,
NodeVersion: nodeVersion,
})
if err != nil {
return fmt.Errorf("failed to create Grafana service: %w", err)
}
c := RunTest(d, svc, grafana, yarnCache, nodeVersion, runnerFlags)
c, err = c.Sync(ctx)
if err != nil {
return fmt.Errorf("failed to run a11y test suite: %w", err)
c, runErr := RunTest(ctx, d, svc, pa11yConfig, noThresholdFail, pa11yResultsPath)
if runErr != nil {
return fmt.Errorf("failed to run a11y test suite: %w", runErr)
}
code, err := c.ExitCode(ctx)
if err != nil {
return fmt.Errorf("failed to get exit code of a11y test suite: %w", err)
c, syncErr := c.Sync(ctx)
if syncErr != nil {
return fmt.Errorf("failed to sync a11y test suite: %w", syncErr)
}
if code != 0 {
code, codeErr := c.ExitCode(ctx)
if codeErr != nil {
return fmt.Errorf("failed to get exit code of a11y test suite: %w", codeErr)
}
if code == 0 {
log.Printf("a11y tests passed with exit code %d", code)
} else if noThresholdFail {
log.Printf("a11y tests failed with exit code %d, but noFail is true", code)
} else {
return fmt.Errorf("a11y tests failed with exit code %d", code)
}
+36 -13
View File
@@ -1,25 +1,48 @@
package main
import (
"context"
"fmt"
"dagger.io/dagger"
)
func RunTest(
ctx context.Context,
d *dagger.Client,
svc *dagger.Service,
src *dagger.Directory, cache *dagger.CacheVolume,
nodeVersion, runnerFlags string) *dagger.Container {
command := fmt.Sprintf(
"./e2e-runner a11y --start-grafana=false"+
" --grafana-host grafana --grafana-port 3001 %s", runnerFlags)
return GrafanaFrontend(d, cache, nodeVersion, src).
WithExec([]string{"/bin/sh", "-c", "apt-get update && apt-get install -y git curl"}).
WithExec([]string{"curl", "-LO", "https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb"}).
WithExec([]string{"apt-get", "install", "-y", "./google-chrome-stable_current_amd64.deb"}).
grafanaService *dagger.Service,
pa11yConfig *dagger.File,
noThresholdFail bool,
pa11yResultsPath string,
) (*dagger.Container, error) {
// docker-puppeteer container already has Chrome and Pa11y installed in it
pa11yContainer := d.Container().From("grafana/docker-puppeteer:1.1.0").
WithWorkdir("/src").
WithServiceBinding("grafana", svc).
WithExec([]string{"/bin/bash", "-c", command}, dagger.ContainerWithExecOpts{Expect: dagger.ReturnTypeAny})
WithExec([]string{"mkdir", "-p", "./screenshots"}). // not yet exported
WithEnvVariable("HOST", grafanaHost).
WithEnvVariable("PORT", fmt.Sprint(grafanaPort))
if noThresholdFail {
// This logic is non-intuitive - --no-threshold-fail will make pa11y fail (by removing thresholds from the config)
// so it can write all violations to the results file. This failure is then ignored by the caller in main.go.
// Otherwise, pa11y ignores violations if they're within the thresholds and doesn't include them in the results file
pa11yContainer = pa11yContainer.
WithEnvVariable("NO_THRESHOLDS", "true")
}
pa11yContainer = pa11yContainer.
WithServiceBinding(grafanaHost, grafanaService).
WithMountedFile("pa11yci-config.js", pa11yConfig).
WithExec([]string{"pa11y-ci", "--config", "pa11yci-config.js"}, dagger.ContainerWithExecOpts{
Expect: dagger.ReturnTypeAny, // allow this to fail here so we can handle non-zero exit codes at the caller
})
if pa11yResultsPath != "" {
_, err := pa11yContainer.File("/src/pa11y-ci-results.json").Export(ctx, pa11yResultsPath)
if err != nil {
return nil, fmt.Errorf("failed to get pa11y results: %w", err)
}
}
return pa11yContainer, nil
}
+7 -53
View File
@@ -9,77 +9,31 @@ import (
"dagger.io/dagger"
)
func NodeImage(version string) string {
return fmt.Sprintf("node:%s-slim", strings.TrimPrefix(strings.TrimSpace(version), "v"))
}
type GrafanaServiceOpts struct {
GrafanaDir *dagger.Directory
HostSrc *dagger.Directory
GrafanaTarGz *dagger.File
License *dagger.File
YarnCache *dagger.CacheVolume
NodeVersion string
}
func Frontend(src *dagger.Directory) *dagger.Directory {
return src.
WithoutFile("go.mod").
WithoutFile("go.sum").
WithoutFile("go.work").
WithoutFile("go.work.sum").
WithoutDirectory(".github").
WithoutDirectory("docs").
WithoutDirectory("pkg").
WithoutDirectory("apps").
WithoutDirectory("videos")
}
func WithGrafanaFrontend(c *dagger.Container, src *dagger.Directory) *dagger.Container {
return c.WithDirectory("/src", Frontend(src), dagger.ContainerWithDirectoryOpts{
Exclude: []string{
"*drone*",
"*.go",
"*.md",
},
})
}
func WithYarnCache(c *dagger.Container, cache *dagger.CacheVolume) *dagger.Container {
return c.
WithWorkdir("/src").
WithMountedCache("/yarn/cache", cache)
}
func GrafanaFrontend(d *dagger.Client, yarnCache *dagger.CacheVolume, nodeVersion string, grafanaDir *dagger.Directory) *dagger.Container {
container := d.Container().From(NodeImage(nodeVersion))
container = WithGrafanaFrontend(container, grafanaDir)
return WithYarnCache(container, yarnCache).
WithEnvVariable("YARN_CACHE_FOLDER", "/yarn/cache").
WithExec([]string{"yarn", "install", "--immutable"})
}
func GrafanaService(ctx context.Context, d *dagger.Client, opts GrafanaServiceOpts) (*dagger.Service, error) {
src := GrafanaFrontend(d, opts.YarnCache, opts.NodeVersion, opts.GrafanaDir)
container := d.Container().From("alpine:3").
WithExec([]string{"apk", "add", "--no-cache", "bash", "tar", "netcat-openbsd"}).
WithMountedFile("/src/grafana.tar.gz", opts.GrafanaTarGz).
WithExec([]string{"mkdir", "-p", "/src/grafana"}).
WithExec([]string{"tar", "--strip-components=1", "-xzf", "/src/grafana.tar.gz", "-C", "/src/grafana"}).
WithDirectory("/src/grafana/devenv", src.Directory("/src/devenv")).
WithDirectory("/src/grafana/e2e", src.Directory("/src/e2e")).
WithDirectory("/src/grafana/scripts", src.Directory("/src/scripts")).
WithDirectory("/src/grafana/tools", src.Directory("/src/tools")).
WithDirectory("/src/grafana/devenv", opts.HostSrc.Directory("./devenv")).
WithDirectory("/src/grafana/e2e/test-plugins", opts.HostSrc.Directory("./e2e/test-plugins")).
WithDirectory("/src/grafana/scripts", opts.HostSrc.Directory("./scripts")).
WithWorkdir("/src/grafana").
WithEnvVariable("GF_APP_MODE", "development").
WithEnvVariable("GF_SERVER_HTTP_PORT", "3001").
WithEnvVariable("GF_SERVER_HTTP_PORT", fmt.Sprint(grafanaPort)).
WithEnvVariable("GF_SERVER_ROUTER_LOGGING", "1").
WithExposedPort(3001)
WithExposedPort(grafanaPort)
var licenseArg string
if opts.License != nil {
container = container.WithMountedFile("/src/license.jwt", opts.License)
licenseArg = "/src/license.jwt"
container = container.WithMountedFile(licenseArg, opts.License)
}
// We add all GF_ environment variables to allow for overriding Grafana configuration.
@@ -3,6 +3,7 @@ package artifacts
import (
"context"
"log/slog"
"os"
"path/filepath"
"dagger.io/dagger"
@@ -191,10 +192,17 @@ func NewBackendFromString(ctx context.Context, log *slog.Logger, artifact string
return nil, err
}
goCacheProg := ""
// If the caller has GOCACHEPROG set, then reuse it
if val, ok := os.LookupEnv("GOCACHEPROG"); ok {
goCacheProg = val
}
bopts := &backend.BuildOpts{
Version: p.Version,
Enterprise: p.Enterprise,
ExperimentalFlags: experiments,
GoCacheProg: goCacheProg,
Static: static,
WireTag: wireTag,
Tags: tags,
+5
View File
@@ -16,6 +16,7 @@ type BuildOpts struct {
ExperimentalFlags []string
Tags []string
WireTag string
GoCacheProg string
Static bool
Enterprise bool
}
@@ -154,6 +155,10 @@ func Builder(
WithMountedCache("/root/.cache/go", goBuildCache).
WithEnvVariable("GOCACHE", "/root/.cache/go")
if prog := opts.GoCacheProg; prog != "" {
builder = builder.WithEnvVariable("GOCACHEPROG", prog)
}
commitInfo := GetVCSInfo(src, version, opts.Enterprise)
builder = withCue(builder, src).
+3 -3
View File
@@ -31,7 +31,7 @@ require (
golang.org/x/sync v0.15.0 // @grafana/alerting-backend
golang.org/x/text v0.26.0 // indirect; @grafana/grafana-backend-group
golang.org/x/time v0.11.0 // indirect; @grafana/grafana-backend-group
google.golang.org/api v0.229.0 // @grafana/grafana-backend-group
google.golang.org/api v0.233.0 // @grafana/grafana-backend-group
google.golang.org/grpc v1.73.0 // indirect; @grafana/plugins-platform-backend
google.golang.org/protobuf v1.36.6 // indirect; @grafana/plugins-platform-backend
gopkg.in/yaml.v3 v3.0.1 // @grafana/alerting-backend
@@ -39,7 +39,7 @@ require (
require (
cloud.google.com/go v0.120.0 // indirect
cloud.google.com/go/auth v0.16.0 // indirect
cloud.google.com/go/auth v0.16.1 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.6.0 // indirect
cloud.google.com/go/iam v1.5.0 // indirect
@@ -67,7 +67,7 @@ require (
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/otel/metric v1.36.0 // indirect
go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect
golang.org/x/sys v0.33.0 // indirect
+6 -6
View File
@@ -3,8 +3,8 @@ cel.dev/expr v0.23.1/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.120.0 h1:wc6bgG9DHyKqF5/vQvX1CiZrtHnxJjBlKUyF9nP6meA=
cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q=
cloud.google.com/go/auth v0.16.0 h1:Pd8P1s9WkcrBE2n/PhAwKsdrR35V3Sg2II9B+ndM3CU=
cloud.google.com/go/auth v0.16.0/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI=
cloud.google.com/go/auth v0.16.1 h1:XrXauHMd30LhQYVRHLGvJiYeczweKQXZxsTbV9TiguU=
cloud.google.com/go/auth v0.16.1/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
@@ -267,8 +267,8 @@ go.opentelemetry.io/contrib/detectors/gcp v1.35.0 h1:bGvFt68+KTiAKFlacHW6AhA56GF
go.opentelemetry.io/contrib/detectors/gcp v1.35.0/go.mod h1:qGWP8/+ILwMRIUf9uIVLloR1uo5ZYAslM4O6OqUi1DA=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2 h1:06ZeJRe5BnYXceSM9Vya83XXVaNGe3H1QqsvqRANQq8=
@@ -370,8 +370,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.229.0 h1:p98ymMtqeJ5i3lIBMj5MpR9kzIIgzpHHh8vQ+vgAzx8=
google.golang.org/api v0.229.0/go.mod h1:wyDfmq5g1wYJWn29O22FDWN48P7Xcz0xz+LBpptYvB0=
google.golang.org/api v0.233.0 h1:iGZfjXAJiUFSSaekVB7LzXl6tRfEKhUN7FkZN++07tI=
google.golang.org/api v0.233.0/go.mod h1:TCIVLLlcwunlMpZIhIp7Ltk77W+vUSdUKAAIlbxY44c=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
@@ -9,6 +9,7 @@ import (
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/urfave/cli/v2"
"k8s.io/apimachinery/pkg/runtime/schema"
authlib "github.com/grafana/authlib/types"
@@ -16,6 +17,7 @@ import (
dashboard "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1"
folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/utils"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/tracing"
@@ -52,8 +54,10 @@ func ToUnifiedStorage(c utils.CommandLine, cfg *setting.Cfg, sqlStore db.DB) err
},
LargeObjects: nil, // TODO... from config
Progress: func(count int, msg string) {
if count < 1 || time.Since(last) > time.Second {
fmt.Printf("[%4d] %s\n", count, msg)
const minInterval = time.Second
shouldPrint := count < 1 || time.Since(last) > minInterval
if shouldPrint {
logger.Info(fmt.Sprintf("[%4d] %s", count, msg))
last = time.Now()
}
},
@@ -67,25 +71,30 @@ func ToUnifiedStorage(c utils.CommandLine, cfg *setting.Cfg, sqlStore db.DB) err
migrator := legacy.NewDashboardAccess(
legacysql.NewDatabaseProvider(sqlStore),
authlib.OrgNamespaceFormatter,
nil, provisioning, nil, sort.ProvideService(),
nil, // no dashboards.Store
provisioning,
nil, // no librarypanels.Service
sort.ProvideService(),
)
if c.Bool("non-interactive") {
client, err := newUnifiedClient(cfg, sqlStore)
if err != nil {
return err
}
client, err := newUnifiedClient(cfg, sqlStore)
if err != nil {
return err
}
if c.Bool("non-interactive") {
opts.Store = client
opts.BlobStore = client
opts.WithHistory = true // always include history in non-interactive mode
rsp, err := migrator.Migrate(ctx, opts)
if err != nil {
return err
if exitErr := handleMigrationError(err, rsp); exitErr != nil {
return exitErr
}
fmt.Printf("Unified storage export: %s\n", time.Since(start))
logger.Info("Migrated legacy resources successfully in", time.Since(start))
if rsp != nil {
jj, _ := json.MarshalIndent(rsp, "", " ")
fmt.Printf("%s\n", string(jj))
logger.Info("Migration summary:", string(jj))
}
return nil
}
@@ -146,11 +155,6 @@ func ToUnifiedStorage(c utils.CommandLine, cfg *setting.Cfg, sqlStore db.DB) err
return err
}
if yes {
client, err := newUnifiedClient(cfg, sqlStore)
if err != nil {
return err
}
// Check the stats (eventually compare)
req := &resourcepb.ResourceStatsRequest{
Namespace: opts.Namespace,
@@ -211,9 +215,14 @@ func promptYesNo(prompt string) (bool, error) {
}
func newUnifiedClient(cfg *setting.Cfg, sqlStore db.DB) (resource.ResourceClient, error) {
featureManager, err := featuremgmt.ProvideManagerService(cfg)
if err != nil {
return nil, err
}
featureToggles := featuremgmt.ProvideToggles(featureManager)
return unified.ProvideUnifiedStorageClient(&unified.Options{
Cfg: cfg,
Features: featuremgmt.WithFeatures(), // none??
Features: featureToggles,
DB: sqlStore,
Tracer: tracing.NewNoopTracerService(),
Reg: prometheus.NewPedanticRegistry(),
@@ -230,3 +239,19 @@ func newParquetClient(file *os.File) (resourcepb.BulkStoreClient, error) {
client := parquet.NewBulkResourceWriterClient(writer)
return client, nil
}
func handleMigrationError(err error, rsp *resourcepb.BulkResponse) error {
if err != nil {
return cli.Exit(fmt.Sprintf("Failed to migrate legacy resources: %+v", err), 1)
}
if rsp != nil && rsp.Error != nil {
msg := fmt.Sprintf("Failed to migrate legacy resources: %s", rsp.Error.Message)
if rsp.Error.Reason != "" {
msg += fmt.Sprintf(" (%s)", rsp.Error.Reason)
}
return cli.Exit(msg, 1)
}
return nil
}
+1
View File
@@ -48,4 +48,5 @@ import (
_ "github.com/grafana/e2e"
_ "github.com/grafana/gofpdf"
_ "github.com/grafana/gomemcache/memcache"
_ "github.com/spyzhov/ajson"
)
@@ -45,7 +45,7 @@ var (
datasourceResponseGauge = promauto.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: "plugins",
Namespace: "grafana",
Name: "datasource_response_size",
Help: "gauge of external data source response sizes returned to Grafana in bytes",
}, []string{"datasource", "datasource_type", "secure_socks_ds_proxy_enabled"},
@@ -94,7 +94,7 @@ func NewRegistry(store map[string]backendplugin.PluginFactoryFunc) *Registry {
}
}
func ProvideCoreRegistry(tracer tracing.Tracer, am *azuremonitor.Service, cw *cloudwatch.CloudWatchService, cm *cloudmonitoring.Service,
func ProvideCoreRegistry(tracer tracing.Tracer, am *azuremonitor.Service, cw *cloudwatch.Service, cm *cloudmonitoring.Service,
es *elasticsearch.Service, grap *graphite.Service, idb *influxdb.Service, lk *loki.Service, otsdb *opentsdb.Service,
pr *prometheus.Service, t *tempo.Service, td *testdatasource.Service, pg *postgres.Service, my *mysql.Service,
ms *mssql.Service, graf *grafanads.Service, pyroscope *pyroscope.Service, parca *parca.Service, zipkin *zipkin.Service, jaeger *jaeger.Service) *Registry {
@@ -102,7 +102,7 @@ func ProvideCoreRegistry(tracer tracing.Tracer, am *azuremonitor.Service, cw *cl
sdktracing.InitDefaultTracer(tracer)
return NewRegistry(map[string]backendplugin.PluginFactoryFunc{
CloudWatch: asBackendPlugin(cw.Executor),
CloudWatch: asBackendPlugin(cw),
CloudMonitoring: asBackendPlugin(cm),
AzureMonitor: asBackendPlugin(am),
Elasticsearch: asBackendPlugin(es),
@@ -217,7 +217,7 @@ func NewPlugin(pluginID string, cfg *setting.Cfg, httpClientProvider *httpclient
jsonData.AliasIDs = append(jsonData.AliasIDs, TestDataAlias)
svc = testdatasource.ProvideService()
case CloudWatch:
svc = cloudwatch.ProvideService(httpClientProvider).Executor
svc = cloudwatch.ProvideService()
case CloudMonitoring:
svc = cloudmonitoring.ProvideService(httpClientProvider)
case AzureMonitor:
+7 -9
View File
@@ -4,7 +4,7 @@ go 1.24.4
require (
github.com/grafana/dskit v0.0.0-20250611075409-46f51e1ce914
github.com/grafana/grafana-plugin-sdk-go v0.277.0
github.com/grafana/grafana-plugin-sdk-go v0.278.0
github.com/json-iterator/go v1.1.12
github.com/prometheus/client_golang v1.22.0
github.com/prometheus/common v0.64.0
@@ -20,7 +20,7 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 // indirect
github.com/BurntSushi/toml v1.5.0 // indirect
github.com/apache/arrow-go/v18 v18.2.0 // indirect
github.com/apache/arrow-go/v18 v18.3.0 // indirect
github.com/aws/aws-sdk-go v1.55.7 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
@@ -28,7 +28,7 @@ require (
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cheekybits/genny v1.0.0 // indirect
github.com/chromedp/cdproto v0.0.0-20240810084448-b931b754e476 // indirect
github.com/chromedp/cdproto v0.0.0-20250429231605-6ed5b53462d4 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dennwc/varint v1.0.0 // indirect
@@ -48,7 +48,6 @@ require (
github.com/gogo/googleapis v1.4.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v1.0.0 // indirect
github.com/google/flatbuffers v25.2.10+incompatible // indirect
github.com/google/gnostic-models v0.6.9 // indirect
github.com/google/go-cmp v0.7.0 // indirect
@@ -57,7 +56,7 @@ require (
github.com/grafana/otel-profiling-go v0.5.1 // indirect
github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // indirect
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 // indirect
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // indirect
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
github.com/hashicorp/go-hclog v1.6.3 // indirect
@@ -105,8 +104,8 @@ require (
go.mongodb.org/mongo-driver v1.16.1 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 // indirect
go.opentelemetry.io/contrib/propagators/jaeger v1.35.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.61.0 // indirect
go.opentelemetry.io/contrib/propagators/jaeger v1.36.0 // indirect
go.opentelemetry.io/contrib/samplers/jaegerremote v0.30.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 // indirect
@@ -122,8 +121,7 @@ require (
golang.org/x/text v0.26.0 // indirect
golang.org/x/tools v0.34.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
gonum.org/v1/gonum v0.16.0 // indirect
google.golang.org/api v0.229.0 // indirect
google.golang.org/api v0.233.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect
google.golang.org/grpc v1.73.0 // indirect
+18 -18
View File
@@ -1,6 +1,6 @@
cloud.google.com/go v0.118.0 h1:tvZe1mgqRxpiVa3XlIGMiPcEUbP1gNXELgD4y/IXmeQ=
cloud.google.com/go/auth v0.16.0 h1:Pd8P1s9WkcrBE2n/PhAwKsdrR35V3Sg2II9B+ndM3CU=
cloud.google.com/go/auth v0.16.0/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI=
cloud.google.com/go/auth v0.16.1 h1:XrXauHMd30LhQYVRHLGvJiYeczweKQXZxsTbV9TiguU=
cloud.google.com/go/auth v0.16.1/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
@@ -21,8 +21,8 @@ github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vS
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/apache/arrow-go/v18 v18.2.0 h1:QhWqpgZMKfWOniGPhbUxrHohWnooGURqL2R2Gg4SO1Q=
github.com/apache/arrow-go/v18 v18.2.0/go.mod h1:Ic/01WSwGJWRrdAZcxjBZ5hbApNJ28K96jGYaxzzGUc=
github.com/apache/arrow-go/v18 v18.3.0 h1:Xq4A6dZj9Nu33sqZibzn012LNnewkTUlfKVUFD/RX/I=
github.com/apache/arrow-go/v18 v18.3.0/go.mod h1:eEM1DnUTHhgGAjf/ChvOAQbUQ+EPohtDrArffvUjPg8=
github.com/apache/thrift v0.21.0 h1:tdPmh/ptjE1IJnhbhrcl2++TauVjy242rkV/UzJChnE=
github.com/apache/thrift v0.21.0/go.mod h1:W1H8aR/QRtYNvrPeFXBtobyRkd0/YVhTc6i07XIAgDw=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
@@ -45,8 +45,8 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE=
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
github.com/chromedp/cdproto v0.0.0-20240810084448-b931b754e476 h1:VnjHsRXCRti7Av7E+j4DCha3kf68echfDzQ+wD11SBU=
github.com/chromedp/cdproto v0.0.0-20240810084448-b931b754e476/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
github.com/chromedp/cdproto v0.0.0-20250429231605-6ed5b53462d4 h1:UZdrvid2JFwnvPlUSEFlE794XZL4Jmrj8fuxfcLECJE=
github.com/chromedp/cdproto v0.0.0-20250429231605-6ed5b53462d4/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
@@ -135,16 +135,16 @@ 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-20250611075409-46f51e1ce914 h1:qcSGhr691f1mmPHwg2svGyO40Ex92G02aOyHzP6XHCE=
github.com/grafana/dskit v0.0.0-20250611075409-46f51e1ce914/go.mod h1:OiN4P4aC6LwLzLbEupH3Ue83VfQoNMfG48rsna8jI/E=
github.com/grafana/grafana-plugin-sdk-go v0.277.0 h1:VDU2F4Y5NeRS//ejctdZtsAshrGaEdbtW33FsK0EQss=
github.com/grafana/grafana-plugin-sdk-go v0.277.0/go.mod h1:mAUWg68w5+1f5TLDqagIr8sWr1RT9h7ufJl5NMcWJAU=
github.com/grafana/grafana-plugin-sdk-go v0.278.0 h1:5/rIYparLi02pofdaag8wnjspMMVNCi8cZhC4cdC3Ho=
github.com/grafana/grafana-plugin-sdk-go v0.278.0/go.mod h1:+8NXT/XUJ/89GV6FxGQ366NZ3nU+cAXDMd0OUESF9H4=
github.com/grafana/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=
github.com/grafana/pyroscope-go/godeltaprof v0.1.8/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248=
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk=
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA=
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU=
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o=
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 h1:sGm2vDRFUrQJO/Veii4h4zG2vvqG6uWNkBHSTqXOZk0=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2/go.mod h1:wd1YpapPLivG6nQgbf7ZkG1hhSOXDhhn4MLTknx2aAc=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
@@ -316,12 +316,12 @@ go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJyS
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 h1:0tY123n7CdWMem7MOVdKOt0YfshufLCwfE5Bob+hQuM=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0/go.mod h1:CosX/aS4eHnG9D7nESYpV753l4j9q5j3SL/PUYd2lR8=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
go.opentelemetry.io/contrib/propagators/jaeger v1.35.0 h1:UIrZgRBHUrYRlJ4V419lVb4rs2ar0wFzKNAebaP05XU=
go.opentelemetry.io/contrib/propagators/jaeger v1.35.0/go.mod h1:0ciyFyYZxE6JqRAQvIgGRabKWDUmNdW3GAQb6y/RlFU=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.61.0 h1:lREC4C0ilyP4WibDhQ7Gg2ygAQFP8oR07Fst/5cafwI=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.61.0/go.mod h1:HfvuU0kW9HewH14VCOLImqKvUgONodURG7Alj/IrnGI=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/contrib/propagators/jaeger v1.36.0 h1:SoCgXYF4ISDtNyfLUzsGDaaudZVTx2yJhOyBO0+/GYk=
go.opentelemetry.io/contrib/propagators/jaeger v1.36.0/go.mod h1:VHu48l0YTRKSObdPQ+Sb8xMZvdnJlN7yhHuHoPgNqHM=
go.opentelemetry.io/contrib/samplers/jaegerremote v0.30.0 h1:bQ1Gvah4Sp8z7epSkgJaNTuZm7sutfA6Fji2/7cKFMc=
go.opentelemetry.io/contrib/samplers/jaegerremote v0.30.0/go.mod h1:9b8Q9rH52NgYH3ShiTFB5wf18Vt3RTH/VMB7LDcC1ug=
go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo=
@@ -407,8 +407,8 @@ golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhS
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/api v0.229.0 h1:p98ymMtqeJ5i3lIBMj5MpR9kzIIgzpHHh8vQ+vgAzx8=
google.golang.org/api v0.229.0/go.mod h1:wyDfmq5g1wYJWn29O22FDWN48P7Xcz0xz+LBpptYvB0=
google.golang.org/api v0.233.0 h1:iGZfjXAJiUFSSaekVB7LzXl6tRfEKhUN7FkZN++07tI=
google.golang.org/api v0.233.0/go.mod h1:TCIVLLlcwunlMpZIhIp7Ltk77W+vUSdUKAAIlbxY44c=
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 h1:Kog3KlB4xevJlAcbbbzPfRG0+X9fdoGM+UBRKVz6Wr0=
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34=
+2
View File
@@ -7,6 +7,7 @@ import (
"github.com/grafana/grafana/pkg/registry/apis/featuretoggle"
"github.com/grafana/grafana/pkg/registry/apis/folders"
"github.com/grafana/grafana/pkg/registry/apis/iam"
"github.com/grafana/grafana/pkg/registry/apis/ofrep"
"github.com/grafana/grafana/pkg/registry/apis/provisioning"
"github.com/grafana/grafana/pkg/registry/apis/query"
"github.com/grafana/grafana/pkg/registry/apis/secret"
@@ -28,6 +29,7 @@ func ProvideRegistryServiceSink(
_ *userstorage.UserStorageAPIBuilder,
_ *secret.SecretAPIBuilder,
_ *provisioning.APIBuilder,
_ *ofrep.APIBuilder,
) *Service {
return &Service{}
}
+25 -9
View File
@@ -59,7 +59,7 @@ type BlobStoreInfo struct {
}
// migrate function -- works for a single kind
type migrator = func(ctx context.Context, orgId int64, opts MigrateOptions, stream resourcepb.BulkStore_BulkProcessClient) (*BlobStoreInfo, error)
type migratorFunc = func(ctx context.Context, orgId int64, opts MigrateOptions, stream resourcepb.BulkStore_BulkProcessClient) (*BlobStoreInfo, error)
func (a *dashboardSqlAccess) Migrate(ctx context.Context, opts MigrateOptions) (*resourcepb.BulkResponse, error) {
info, err := authlib.ParseNamespace(opts.Namespace)
@@ -75,7 +75,7 @@ func (a *dashboardSqlAccess) Migrate(ctx context.Context, opts MigrateOptions) (
return nil, fmt.Errorf("missing resource selector")
}
migrators := []migrator{}
migratorFuncs := []migratorFunc{}
settings := resource.BulkSettings{
RebuildCollection: true,
SkipValidation: true,
@@ -84,7 +84,7 @@ func (a *dashboardSqlAccess) Migrate(ctx context.Context, opts MigrateOptions) (
for _, res := range opts.Resources {
switch fmt.Sprintf("%s/%s", res.Group, res.Resource) {
case "folder.grafana.app/folders":
migrators = append(migrators, a.migrateFolders)
migratorFuncs = append(migratorFuncs, a.migrateFolders)
settings.Collection = append(settings.Collection, &resourcepb.ResourceKey{
Namespace: opts.Namespace,
Group: folders.GROUP,
@@ -92,7 +92,7 @@ func (a *dashboardSqlAccess) Migrate(ctx context.Context, opts MigrateOptions) (
})
case "dashboard.grafana.app/librarypanels":
migrators = append(migrators, a.migratePanels)
migratorFuncs = append(migratorFuncs, a.migratePanels)
settings.Collection = append(settings.Collection, &resourcepb.ResourceKey{
Namespace: opts.Namespace,
Group: dashboard.GROUP,
@@ -100,7 +100,7 @@ func (a *dashboardSqlAccess) Migrate(ctx context.Context, opts MigrateOptions) (
})
case "dashboard.grafana.app/dashboards":
migrators = append(migrators, a.migrateDashboards)
migratorFuncs = append(migratorFuncs, a.migrateDashboards)
settings.Collection = append(settings.Collection, &resourcepb.ResourceKey{
Namespace: opts.Namespace,
Group: dashboard.GROUP,
@@ -110,12 +110,22 @@ func (a *dashboardSqlAccess) Migrate(ctx context.Context, opts MigrateOptions) (
return nil, fmt.Errorf("unsupported resource: %s", res)
}
}
if opts.OnlyCount {
return a.countValues(ctx, opts)
}
ctx = metadata.NewOutgoingContext(ctx, settings.ToMD())
if md, ok := metadata.FromOutgoingContext(ctx); ok {
a.log.Debug("bulk grpc request metadata",
"metadata", md,
"collection", settings.Collection,
)
} else {
a.log.Debug("bulk grpc request, no metadata found",
"collection", settings.Collection,
)
}
stream, err := opts.Store.BulkProcess(ctx)
if err != nil {
return nil, err
@@ -123,9 +133,11 @@ func (a *dashboardSqlAccess) Migrate(ctx context.Context, opts MigrateOptions) (
// Now run each migration
blobStore := BlobStoreInfo{}
for _, m := range migrators {
a.log.Info("start migrating legacy resources", "namespace", opts.Namespace, "orgId", info.OrgID, "stackId", info.StackID)
for _, m := range migratorFuncs {
blobs, err := m(ctx, info.OrgID, opts, stream)
if err != nil {
a.log.Error("error migrating legacy resources", "error", err, "namespace", opts.Namespace)
return nil, err
}
if blobs != nil {
@@ -133,7 +145,7 @@ func (a *dashboardSqlAccess) Migrate(ctx context.Context, opts MigrateOptions) (
blobStore.Size += blobs.Size
}
}
fmt.Printf("BLOBS: %+v\n", blobStore)
a.log.Info("finished migrating legacy resources", "blobStore", blobStore)
return stream.CloseAndRecv()
}
@@ -291,7 +303,11 @@ func (a *dashboardSqlAccess) migrateDashboards(ctx context.Context, orgId int64,
if len(rows.rejected) > 0 {
for _, row := range rows.rejected {
id := row.Dash.Labels[utils.LabelKeyDeprecatedInternalID]
fmt.Printf("REJECTED: %s / %s\n", id, row.Dash.Name)
a.log.Warn("rejected dashboard",
"dashboard", row.Dash.Name,
"uid", row.Dash.UID,
"id", id,
)
opts.Progress(-2, fmt.Sprintf("rejected: id:%s, uid:%s", id, row.Dash.Name))
}
}
@@ -1,20 +1,33 @@
SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.deleted, plugin_id,
dashboard.org_id,
dashboard.id,
dashboard.uid,
dashboard.folder_uid,
dashboard.deleted,
plugin_id,
provisioning.name as repo_name,
provisioning.external_id as repo_path,
provisioning.check_sum as repo_hash,
provisioning.updated as repo_ts,
dashboard.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard.created,
created_user.uid as created_by,
dashboard.created_by as created_by_id,
{{ if .Query.UseHistoryTable }}
dashboard_version.created, updated_user.uid as updated_by,updated_user.id as created_by_id,
dashboard_version.version, dashboard_version.message,
dashboard_version.data, dashboard_version.api_version
dashboard_version.created,
updated_user.uid as updated_by,
updated_user.id as created_by_id,
dashboard_version.version,
dashboard_version.message,
dashboard_version.data,
dashboard_version.api_version
{{ else }}
dashboard.updated, updated_user.uid as updated_by, dashboard.updated_by as updated_by_id,
dashboard.version, '' as message,
dashboard.data, dashboard.api_version
dashboard.updated,
updated_user.uid as updated_by,
dashboard.updated_by as updated_by_id,
dashboard.version,
'' as message,
dashboard.data,
dashboard.api_version
{{ end }}
FROM {{ .Ident .DashboardTable }} as dashboard
{{ if .Query.UseHistoryTable }}
@@ -39,15 +52,15 @@ WHERE dashboard.is_folder = {{ .Arg .Query.GetFolders }}
dashboard_version.version {{ .Query.Order }},
dashboard.uid ASC
{{ else }}
{{ if .Query.UID }}
AND dashboard.uid = {{ .Arg .Query.UID }}
{{ else if .Query.LastID }}
AND dashboard.id < {{ .Arg .Query.LastID }}
{{ end }}
{{ if .Query.GetTrash }}
AND dashboard.deleted IS NOT NULL
{{ else }}
AND dashboard.deleted IS NULL
{{ end }}
{{ if .Query.UID }}
AND dashboard.uid = {{ .Arg .Query.UID }}
{{ else if .Query.LastID }}
AND dashboard.id < {{ .Arg .Query.LastID }}
{{ end }}
{{ if .Query.GetTrash }}
AND dashboard.deleted IS NOT NULL
{{ else }}
AND dashboard.deleted IS NULL
{{ end }}
ORDER BY dashboard.id DESC
{{ end }}
@@ -1,20 +1,29 @@
SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.deleted, plugin_id,
dashboard.org_id,
dashboard.id,
dashboard.uid,
dashboard.folder_uid,
dashboard.deleted,
plugin_id,
provisioning.name as repo_name,
provisioning.external_id as repo_path,
provisioning.check_sum as repo_hash,
provisioning.updated as repo_ts,
dashboard.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard.updated, updated_user.uid as updated_by, dashboard.updated_by as updated_by_id,
dashboard.version, '' as message,
dashboard.data, dashboard.api_version
dashboard.created,
created_user.uid as created_by,
dashboard.created_by as created_by_id,
dashboard.updated,
updated_user.uid as updated_by,
dashboard.updated_by as updated_by_id,
dashboard.version,
'' as message,
dashboard.data,
dashboard.api_version
FROM `grafana`.`dashboard` as dashboard
LEFT OUTER JOIN `grafana`.`dashboard_provisioning` as provisioning ON dashboard.id = provisioning.dashboard_id
LEFT OUTER JOIN `grafana`.`user` as created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN `grafana`.`user` as updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = FALSE
AND dashboard.org_id = 2
AND dashboard.deleted IS NULL
AND dashboard.deleted IS NULL
ORDER BY dashboard.id DESC
@@ -1,21 +1,30 @@
SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.deleted, plugin_id,
dashboard.org_id,
dashboard.id,
dashboard.uid,
dashboard.folder_uid,
dashboard.deleted,
plugin_id,
provisioning.name as repo_name,
provisioning.external_id as repo_path,
provisioning.check_sum as repo_hash,
provisioning.updated as repo_ts,
dashboard.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard.updated, updated_user.uid as updated_by, dashboard.updated_by as updated_by_id,
dashboard.version, '' as message,
dashboard.data, dashboard.api_version
dashboard.created,
created_user.uid as created_by,
dashboard.created_by as created_by_id,
dashboard.updated,
updated_user.uid as updated_by,
dashboard.updated_by as updated_by_id,
dashboard.version,
'' as message,
dashboard.data,
dashboard.api_version
FROM `grafana`.`dashboard` as dashboard
LEFT OUTER JOIN `grafana`.`dashboard_provisioning` as provisioning ON dashboard.id = provisioning.dashboard_id
LEFT OUTER JOIN `grafana`.`user` as created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN `grafana`.`user` as updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = FALSE
AND dashboard.org_id = 2
AND dashboard.id < 22
AND dashboard.deleted IS NULL
AND dashboard.id < 22
AND dashboard.deleted IS NULL
ORDER BY dashboard.id DESC
@@ -1,15 +1,24 @@
SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.deleted, plugin_id,
dashboard.org_id,
dashboard.id,
dashboard.uid,
dashboard.folder_uid,
dashboard.deleted,
plugin_id,
provisioning.name as repo_name,
provisioning.external_id as repo_path,
provisioning.check_sum as repo_hash,
provisioning.updated as repo_ts,
dashboard.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard_version.created, updated_user.uid as updated_by,updated_user.id as created_by_id,
dashboard_version.version, dashboard_version.message,
dashboard_version.data, dashboard_version.api_version
dashboard.created,
created_user.uid as created_by,
dashboard.created_by as created_by_id,
dashboard_version.created,
updated_user.uid as updated_by,
updated_user.id as created_by_id,
dashboard_version.version,
dashboard_version.message,
dashboard_version.data,
dashboard_version.api_version
FROM `grafana`.`dashboard` as dashboard
LEFT OUTER JOIN `grafana`.`dashboard_version` as dashboard_version ON dashboard.id = dashboard_version.dashboard_id
LEFT OUTER JOIN `grafana`.`dashboard_provisioning` as provisioning ON dashboard.id = provisioning.dashboard_id
@@ -1,20 +1,29 @@
SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.deleted, plugin_id,
dashboard.org_id,
dashboard.id,
dashboard.uid,
dashboard.folder_uid,
dashboard.deleted,
plugin_id,
provisioning.name as repo_name,
provisioning.external_id as repo_path,
provisioning.check_sum as repo_hash,
provisioning.updated as repo_ts,
dashboard.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard.updated, updated_user.uid as updated_by, dashboard.updated_by as updated_by_id,
dashboard.version, '' as message,
dashboard.data, dashboard.api_version
dashboard.created,
created_user.uid as created_by,
dashboard.created_by as created_by_id,
dashboard.updated,
updated_user.uid as updated_by,
dashboard.updated_by as updated_by_id,
dashboard.version,
'' as message,
dashboard.data,
dashboard.api_version
FROM `grafana`.`dashboard` as dashboard
LEFT OUTER JOIN `grafana`.`dashboard_provisioning` as provisioning ON dashboard.id = provisioning.dashboard_id
LEFT OUTER JOIN `grafana`.`user` as created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN `grafana`.`user` as updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = TRUE
AND dashboard.org_id = 2
AND dashboard.deleted IS NULL
AND dashboard.deleted IS NULL
ORDER BY dashboard.id DESC
@@ -1,21 +1,30 @@
SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.deleted, plugin_id,
dashboard.org_id,
dashboard.id,
dashboard.uid,
dashboard.folder_uid,
dashboard.deleted,
plugin_id,
provisioning.name as repo_name,
provisioning.external_id as repo_path,
provisioning.check_sum as repo_hash,
provisioning.updated as repo_ts,
dashboard.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard.updated, updated_user.uid as updated_by, dashboard.updated_by as updated_by_id,
dashboard.version, '' as message,
dashboard.data, dashboard.api_version
dashboard.created,
created_user.uid as created_by,
dashboard.created_by as created_by_id,
dashboard.updated,
updated_user.uid as updated_by,
dashboard.updated_by as updated_by_id,
dashboard.version,
'' as message,
dashboard.data,
dashboard.api_version
FROM `grafana`.`dashboard` as dashboard
LEFT OUTER JOIN `grafana`.`dashboard_provisioning` as provisioning ON dashboard.id = provisioning.dashboard_id
LEFT OUTER JOIN `grafana`.`user` as created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN `grafana`.`user` as updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = FALSE
AND dashboard.org_id = 2
AND dashboard.uid = 'UUU'
AND dashboard.deleted IS NULL
AND dashboard.uid = 'UUU'
AND dashboard.deleted IS NULL
ORDER BY dashboard.id DESC
@@ -1,15 +1,24 @@
SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.deleted, plugin_id,
dashboard.org_id,
dashboard.id,
dashboard.uid,
dashboard.folder_uid,
dashboard.deleted,
plugin_id,
provisioning.name as repo_name,
provisioning.external_id as repo_path,
provisioning.check_sum as repo_hash,
provisioning.updated as repo_ts,
dashboard.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard_version.created, updated_user.uid as updated_by,updated_user.id as created_by_id,
dashboard_version.version, dashboard_version.message,
dashboard_version.data, dashboard_version.api_version
dashboard.created,
created_user.uid as created_by,
dashboard.created_by as created_by_id,
dashboard_version.created,
updated_user.uid as updated_by,
updated_user.id as created_by_id,
dashboard_version.version,
dashboard_version.message,
dashboard_version.data,
dashboard_version.api_version
FROM `grafana`.`dashboard` as dashboard
LEFT OUTER JOIN `grafana`.`dashboard_version` as dashboard_version ON dashboard.id = dashboard_version.dashboard_id
LEFT OUTER JOIN `grafana`.`dashboard_provisioning` as provisioning ON dashboard.id = provisioning.dashboard_id
@@ -1,21 +1,30 @@
SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.deleted, plugin_id,
dashboard.org_id,
dashboard.id,
dashboard.uid,
dashboard.folder_uid,
dashboard.deleted,
plugin_id,
provisioning.name as repo_name,
provisioning.external_id as repo_path,
provisioning.check_sum as repo_hash,
provisioning.updated as repo_ts,
dashboard.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard.updated, updated_user.uid as updated_by, dashboard.updated_by as updated_by_id,
dashboard.version, '' as message,
dashboard.data, dashboard.api_version
dashboard.created,
created_user.uid as created_by,
dashboard.created_by as created_by_id,
dashboard.updated,
updated_user.uid as updated_by,
dashboard.updated_by as updated_by_id,
dashboard.version,
'' as message,
dashboard.data,
dashboard.api_version
FROM `grafana`.`dashboard` as dashboard
LEFT OUTER JOIN `grafana`.`dashboard_provisioning` as provisioning ON dashboard.id = provisioning.dashboard_id
LEFT OUTER JOIN `grafana`.`user` as created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN `grafana`.`user` as updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = FALSE
AND dashboard.org_id = 2
AND dashboard.uid = 'UUU'
AND dashboard.deleted IS NULL
AND dashboard.uid = 'UUU'
AND dashboard.deleted IS NULL
ORDER BY dashboard.id DESC
@@ -1,20 +1,29 @@
SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.deleted, plugin_id,
dashboard.org_id,
dashboard.id,
dashboard.uid,
dashboard.folder_uid,
dashboard.deleted,
plugin_id,
provisioning.name as repo_name,
provisioning.external_id as repo_path,
provisioning.check_sum as repo_hash,
provisioning.updated as repo_ts,
dashboard.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard.updated, updated_user.uid as updated_by, dashboard.updated_by as updated_by_id,
dashboard.version, '' as message,
dashboard.data, dashboard.api_version
dashboard.created,
created_user.uid as created_by,
dashboard.created_by as created_by_id,
dashboard.updated,
updated_user.uid as updated_by,
dashboard.updated_by as updated_by_id,
dashboard.version,
'' as message,
dashboard.data,
dashboard.api_version
FROM "grafana"."dashboard" as dashboard
LEFT OUTER JOIN "grafana"."dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id
LEFT OUTER JOIN "grafana"."user" as created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "grafana"."user" as updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = FALSE
AND dashboard.org_id = 2
AND dashboard.deleted IS NULL
AND dashboard.deleted IS NULL
ORDER BY dashboard.id DESC
@@ -1,21 +1,30 @@
SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.deleted, plugin_id,
dashboard.org_id,
dashboard.id,
dashboard.uid,
dashboard.folder_uid,
dashboard.deleted,
plugin_id,
provisioning.name as repo_name,
provisioning.external_id as repo_path,
provisioning.check_sum as repo_hash,
provisioning.updated as repo_ts,
dashboard.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard.updated, updated_user.uid as updated_by, dashboard.updated_by as updated_by_id,
dashboard.version, '' as message,
dashboard.data, dashboard.api_version
dashboard.created,
created_user.uid as created_by,
dashboard.created_by as created_by_id,
dashboard.updated,
updated_user.uid as updated_by,
dashboard.updated_by as updated_by_id,
dashboard.version,
'' as message,
dashboard.data,
dashboard.api_version
FROM "grafana"."dashboard" as dashboard
LEFT OUTER JOIN "grafana"."dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id
LEFT OUTER JOIN "grafana"."user" as created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "grafana"."user" as updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = FALSE
AND dashboard.org_id = 2
AND dashboard.id < 22
AND dashboard.deleted IS NULL
AND dashboard.id < 22
AND dashboard.deleted IS NULL
ORDER BY dashboard.id DESC
@@ -1,15 +1,24 @@
SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.deleted, plugin_id,
dashboard.org_id,
dashboard.id,
dashboard.uid,
dashboard.folder_uid,
dashboard.deleted,
plugin_id,
provisioning.name as repo_name,
provisioning.external_id as repo_path,
provisioning.check_sum as repo_hash,
provisioning.updated as repo_ts,
dashboard.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard_version.created, updated_user.uid as updated_by,updated_user.id as created_by_id,
dashboard_version.version, dashboard_version.message,
dashboard_version.data, dashboard_version.api_version
dashboard.created,
created_user.uid as created_by,
dashboard.created_by as created_by_id,
dashboard_version.created,
updated_user.uid as updated_by,
updated_user.id as created_by_id,
dashboard_version.version,
dashboard_version.message,
dashboard_version.data,
dashboard_version.api_version
FROM "grafana"."dashboard" as dashboard
LEFT OUTER JOIN "grafana"."dashboard_version" as dashboard_version ON dashboard.id = dashboard_version.dashboard_id
LEFT OUTER JOIN "grafana"."dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id
@@ -1,20 +1,29 @@
SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.deleted, plugin_id,
dashboard.org_id,
dashboard.id,
dashboard.uid,
dashboard.folder_uid,
dashboard.deleted,
plugin_id,
provisioning.name as repo_name,
provisioning.external_id as repo_path,
provisioning.check_sum as repo_hash,
provisioning.updated as repo_ts,
dashboard.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard.updated, updated_user.uid as updated_by, dashboard.updated_by as updated_by_id,
dashboard.version, '' as message,
dashboard.data, dashboard.api_version
dashboard.created,
created_user.uid as created_by,
dashboard.created_by as created_by_id,
dashboard.updated,
updated_user.uid as updated_by,
dashboard.updated_by as updated_by_id,
dashboard.version,
'' as message,
dashboard.data,
dashboard.api_version
FROM "grafana"."dashboard" as dashboard
LEFT OUTER JOIN "grafana"."dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id
LEFT OUTER JOIN "grafana"."user" as created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "grafana"."user" as updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = TRUE
AND dashboard.org_id = 2
AND dashboard.deleted IS NULL
AND dashboard.deleted IS NULL
ORDER BY dashboard.id DESC
@@ -1,21 +1,30 @@
SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.deleted, plugin_id,
dashboard.org_id,
dashboard.id,
dashboard.uid,
dashboard.folder_uid,
dashboard.deleted,
plugin_id,
provisioning.name as repo_name,
provisioning.external_id as repo_path,
provisioning.check_sum as repo_hash,
provisioning.updated as repo_ts,
dashboard.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard.updated, updated_user.uid as updated_by, dashboard.updated_by as updated_by_id,
dashboard.version, '' as message,
dashboard.data, dashboard.api_version
dashboard.created,
created_user.uid as created_by,
dashboard.created_by as created_by_id,
dashboard.updated,
updated_user.uid as updated_by,
dashboard.updated_by as updated_by_id,
dashboard.version,
'' as message,
dashboard.data,
dashboard.api_version
FROM "grafana"."dashboard" as dashboard
LEFT OUTER JOIN "grafana"."dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id
LEFT OUTER JOIN "grafana"."user" as created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "grafana"."user" as updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = FALSE
AND dashboard.org_id = 2
AND dashboard.uid = 'UUU'
AND dashboard.deleted IS NULL
AND dashboard.uid = 'UUU'
AND dashboard.deleted IS NULL
ORDER BY dashboard.id DESC
@@ -1,15 +1,24 @@
SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.deleted, plugin_id,
dashboard.org_id,
dashboard.id,
dashboard.uid,
dashboard.folder_uid,
dashboard.deleted,
plugin_id,
provisioning.name as repo_name,
provisioning.external_id as repo_path,
provisioning.check_sum as repo_hash,
provisioning.updated as repo_ts,
dashboard.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard_version.created, updated_user.uid as updated_by,updated_user.id as created_by_id,
dashboard_version.version, dashboard_version.message,
dashboard_version.data, dashboard_version.api_version
dashboard.created,
created_user.uid as created_by,
dashboard.created_by as created_by_id,
dashboard_version.created,
updated_user.uid as updated_by,
updated_user.id as created_by_id,
dashboard_version.version,
dashboard_version.message,
dashboard_version.data,
dashboard_version.api_version
FROM "grafana"."dashboard" as dashboard
LEFT OUTER JOIN "grafana"."dashboard_version" as dashboard_version ON dashboard.id = dashboard_version.dashboard_id
LEFT OUTER JOIN "grafana"."dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id
@@ -1,21 +1,30 @@
SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.deleted, plugin_id,
dashboard.org_id,
dashboard.id,
dashboard.uid,
dashboard.folder_uid,
dashboard.deleted,
plugin_id,
provisioning.name as repo_name,
provisioning.external_id as repo_path,
provisioning.check_sum as repo_hash,
provisioning.updated as repo_ts,
dashboard.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard.updated, updated_user.uid as updated_by, dashboard.updated_by as updated_by_id,
dashboard.version, '' as message,
dashboard.data, dashboard.api_version
dashboard.created,
created_user.uid as created_by,
dashboard.created_by as created_by_id,
dashboard.updated,
updated_user.uid as updated_by,
dashboard.updated_by as updated_by_id,
dashboard.version,
'' as message,
dashboard.data,
dashboard.api_version
FROM "grafana"."dashboard" as dashboard
LEFT OUTER JOIN "grafana"."dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id
LEFT OUTER JOIN "grafana"."user" as created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "grafana"."user" as updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = FALSE
AND dashboard.org_id = 2
AND dashboard.uid = 'UUU'
AND dashboard.deleted IS NULL
AND dashboard.uid = 'UUU'
AND dashboard.deleted IS NULL
ORDER BY dashboard.id DESC
@@ -1,20 +1,29 @@
SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.deleted, plugin_id,
dashboard.org_id,
dashboard.id,
dashboard.uid,
dashboard.folder_uid,
dashboard.deleted,
plugin_id,
provisioning.name as repo_name,
provisioning.external_id as repo_path,
provisioning.check_sum as repo_hash,
provisioning.updated as repo_ts,
dashboard.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard.updated, updated_user.uid as updated_by, dashboard.updated_by as updated_by_id,
dashboard.version, '' as message,
dashboard.data, dashboard.api_version
dashboard.created,
created_user.uid as created_by,
dashboard.created_by as created_by_id,
dashboard.updated,
updated_user.uid as updated_by,
dashboard.updated_by as updated_by_id,
dashboard.version,
'' as message,
dashboard.data,
dashboard.api_version
FROM "grafana"."dashboard" as dashboard
LEFT OUTER JOIN "grafana"."dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id
LEFT OUTER JOIN "grafana"."user" as created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "grafana"."user" as updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = FALSE
AND dashboard.org_id = 2
AND dashboard.deleted IS NULL
AND dashboard.deleted IS NULL
ORDER BY dashboard.id DESC
@@ -1,21 +1,30 @@
SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.deleted, plugin_id,
dashboard.org_id,
dashboard.id,
dashboard.uid,
dashboard.folder_uid,
dashboard.deleted,
plugin_id,
provisioning.name as repo_name,
provisioning.external_id as repo_path,
provisioning.check_sum as repo_hash,
provisioning.updated as repo_ts,
dashboard.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard.updated, updated_user.uid as updated_by, dashboard.updated_by as updated_by_id,
dashboard.version, '' as message,
dashboard.data, dashboard.api_version
dashboard.created,
created_user.uid as created_by,
dashboard.created_by as created_by_id,
dashboard.updated,
updated_user.uid as updated_by,
dashboard.updated_by as updated_by_id,
dashboard.version,
'' as message,
dashboard.data,
dashboard.api_version
FROM "grafana"."dashboard" as dashboard
LEFT OUTER JOIN "grafana"."dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id
LEFT OUTER JOIN "grafana"."user" as created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "grafana"."user" as updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = FALSE
AND dashboard.org_id = 2
AND dashboard.id < 22
AND dashboard.deleted IS NULL
AND dashboard.id < 22
AND dashboard.deleted IS NULL
ORDER BY dashboard.id DESC
@@ -1,15 +1,24 @@
SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.deleted, plugin_id,
dashboard.org_id,
dashboard.id,
dashboard.uid,
dashboard.folder_uid,
dashboard.deleted,
plugin_id,
provisioning.name as repo_name,
provisioning.external_id as repo_path,
provisioning.check_sum as repo_hash,
provisioning.updated as repo_ts,
dashboard.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard_version.created, updated_user.uid as updated_by,updated_user.id as created_by_id,
dashboard_version.version, dashboard_version.message,
dashboard_version.data, dashboard_version.api_version
dashboard.created,
created_user.uid as created_by,
dashboard.created_by as created_by_id,
dashboard_version.created,
updated_user.uid as updated_by,
updated_user.id as created_by_id,
dashboard_version.version,
dashboard_version.message,
dashboard_version.data,
dashboard_version.api_version
FROM "grafana"."dashboard" as dashboard
LEFT OUTER JOIN "grafana"."dashboard_version" as dashboard_version ON dashboard.id = dashboard_version.dashboard_id
LEFT OUTER JOIN "grafana"."dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id
@@ -1,20 +1,29 @@
SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.deleted, plugin_id,
dashboard.org_id,
dashboard.id,
dashboard.uid,
dashboard.folder_uid,
dashboard.deleted,
plugin_id,
provisioning.name as repo_name,
provisioning.external_id as repo_path,
provisioning.check_sum as repo_hash,
provisioning.updated as repo_ts,
dashboard.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard.updated, updated_user.uid as updated_by, dashboard.updated_by as updated_by_id,
dashboard.version, '' as message,
dashboard.data, dashboard.api_version
dashboard.created,
created_user.uid as created_by,
dashboard.created_by as created_by_id,
dashboard.updated,
updated_user.uid as updated_by,
dashboard.updated_by as updated_by_id,
dashboard.version,
'' as message,
dashboard.data,
dashboard.api_version
FROM "grafana"."dashboard" as dashboard
LEFT OUTER JOIN "grafana"."dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id
LEFT OUTER JOIN "grafana"."user" as created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "grafana"."user" as updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = TRUE
AND dashboard.org_id = 2
AND dashboard.deleted IS NULL
AND dashboard.deleted IS NULL
ORDER BY dashboard.id DESC
@@ -1,21 +1,30 @@
SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.deleted, plugin_id,
dashboard.org_id,
dashboard.id,
dashboard.uid,
dashboard.folder_uid,
dashboard.deleted,
plugin_id,
provisioning.name as repo_name,
provisioning.external_id as repo_path,
provisioning.check_sum as repo_hash,
provisioning.updated as repo_ts,
dashboard.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard.updated, updated_user.uid as updated_by, dashboard.updated_by as updated_by_id,
dashboard.version, '' as message,
dashboard.data, dashboard.api_version
dashboard.created,
created_user.uid as created_by,
dashboard.created_by as created_by_id,
dashboard.updated,
updated_user.uid as updated_by,
dashboard.updated_by as updated_by_id,
dashboard.version,
'' as message,
dashboard.data,
dashboard.api_version
FROM "grafana"."dashboard" as dashboard
LEFT OUTER JOIN "grafana"."dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id
LEFT OUTER JOIN "grafana"."user" as created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "grafana"."user" as updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = FALSE
AND dashboard.org_id = 2
AND dashboard.uid = 'UUU'
AND dashboard.deleted IS NULL
AND dashboard.uid = 'UUU'
AND dashboard.deleted IS NULL
ORDER BY dashboard.id DESC
@@ -1,15 +1,24 @@
SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.deleted, plugin_id,
dashboard.org_id,
dashboard.id,
dashboard.uid,
dashboard.folder_uid,
dashboard.deleted,
plugin_id,
provisioning.name as repo_name,
provisioning.external_id as repo_path,
provisioning.check_sum as repo_hash,
provisioning.updated as repo_ts,
dashboard.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard_version.created, updated_user.uid as updated_by,updated_user.id as created_by_id,
dashboard_version.version, dashboard_version.message,
dashboard_version.data, dashboard_version.api_version
dashboard.created,
created_user.uid as created_by,
dashboard.created_by as created_by_id,
dashboard_version.created,
updated_user.uid as updated_by,
updated_user.id as created_by_id,
dashboard_version.version,
dashboard_version.message,
dashboard_version.data,
dashboard_version.api_version
FROM "grafana"."dashboard" as dashboard
LEFT OUTER JOIN "grafana"."dashboard_version" as dashboard_version ON dashboard.id = dashboard_version.dashboard_id
LEFT OUTER JOIN "grafana"."dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id
@@ -1,21 +1,30 @@
SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.deleted, plugin_id,
dashboard.org_id,
dashboard.id,
dashboard.uid,
dashboard.folder_uid,
dashboard.deleted,
plugin_id,
provisioning.name as repo_name,
provisioning.external_id as repo_path,
provisioning.check_sum as repo_hash,
provisioning.updated as repo_ts,
dashboard.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard.updated, updated_user.uid as updated_by, dashboard.updated_by as updated_by_id,
dashboard.version, '' as message,
dashboard.data, dashboard.api_version
dashboard.created,
created_user.uid as created_by,
dashboard.created_by as created_by_id,
dashboard.updated,
updated_user.uid as updated_by,
dashboard.updated_by as updated_by_id,
dashboard.version,
'' as message,
dashboard.data,
dashboard.api_version
FROM "grafana"."dashboard" as dashboard
LEFT OUTER JOIN "grafana"."dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id
LEFT OUTER JOIN "grafana"."user" as created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "grafana"."user" as updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = FALSE
AND dashboard.org_id = 2
AND dashboard.uid = 'UUU'
AND dashboard.deleted IS NULL
AND dashboard.uid = 'UUU'
AND dashboard.deleted IS NULL
ORDER BY dashboard.id DESC
@@ -229,6 +229,19 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resourcepb.Reso
return nil, fmt.Errorf("only one repo name is supported")
}
query.ManagerIdentity = vals[0]
case unisearch.DASHBOARD_LIBRARY_PANEL_REFERENCE:
if len(vals) != 1 {
return nil, fmt.Errorf("only one library panel uid is supported")
}
return c.getLibraryPanelConnections(ctx, user, vals[0], req.Options.Key.Namespace)
case resource.SEARCH_FIELD_TITLE_PHRASE:
if len(vals) != 1 {
return nil, fmt.Errorf("only one title supported")
}
query.Title = vals[0]
query.TitleExactMatch = true
}
}
@@ -349,6 +362,34 @@ func getResourceKey(item *dashboards.DashboardSearchProjection, namespace string
}
}
// retrieves all the dashboards that are connected to the given library panel
func (c *DashboardSearchClient) getLibraryPanelConnections(ctx context.Context, user identity.Requester, libraryElementUID, namespace string) (*resourcepb.ResourceSearchResponse, error) {
connections, err := c.dashboardStore.GetDashboardsByLibraryPanelUID(ctx, libraryElementUID, user.GetOrgID())
if err != nil {
return nil, err
}
columns := c.getColumns("", &dashboards.FindPersistedDashboardsQuery{})
list := &resourcepb.ResourceSearchResponse{
Results: &resourcepb.ResourceTable{
Columns: columns,
},
}
for _, dashboard := range connections {
cells := c.createCommonCells("", dashboard.FolderUID, dashboard.ID, nil) // nolint:staticcheck
list.Results.Rows = append(list.Results.Rows, &resourcepb.ResourceTableRow{
Key: getResourceKey(&dashboards.DashboardSearchProjection{
UID: dashboard.UID,
}, namespace),
Cells: cells,
})
}
list.TotalHits = int64(len(list.Results.Rows))
return list, nil
}
func (c *DashboardSearchClient) GetStats(ctx context.Context, req *resourcepb.ResourceStatsRequest, _ ...grpc.CallOption) (*resourcepb.ResourceStatsResponse, error) {
info, err := claims.ParseNamespace(req.Namespace)
if err != nil {
@@ -309,6 +309,38 @@ func TestDashboardSearchClient_Search(t *testing.T) {
}
})
t.Run("When searching for SEARCH_FIELD_TITLE_PHRASE, value should be set as title, and ExactMatch should be enabled", func(t *testing.T) {
mockStore.On("FindDashboards", mock.Anything, &dashboards.FindPersistedDashboardsQuery{
Title: "test",
TitleExactMatch: true,
SignedInUser: user, // user from context should be used
Type: "dash-db", // should set type based off of key
}).Return([]dashboards.DashboardSearchProjection{
{UID: "uid", Title: "Test Dashboard", FolderUID: "folder1"},
}, nil).Once()
req := &resourcepb.ResourceSearchRequest{
Options: &resourcepb.ListOptions{
Key: dashboardKey,
Fields: []*resourcepb.Requirement{
{
Key: resource.SEARCH_FIELD_TITLE_PHRASE, // nolint:staticcheck
Operator: string(selection.Equals),
Values: []string{"test"},
},
},
},
}
resp, err := client.Search(ctx, req)
require.NoError(t, err)
require.NotNil(t, resp)
mockStore.AssertExpectations(t)
for _, row := range resp.Results.Rows {
require.Equal(t, len(row.Cells), len(resp.Results.Columns))
}
})
t.Run("Should read labels for the dashboard ids", func(t *testing.T) {
mockStore.On("FindDashboards", mock.Anything, &dashboards.FindPersistedDashboardsQuery{
DashboardIds: []int64{1, 2},
@@ -540,6 +572,72 @@ func TestDashboardSearchClient_Search(t *testing.T) {
require.Equal(t, []byte(strconv.FormatInt(100, 10)), resp.Results.Rows[0].Cells[i]) // views should be set to 100
mockStore.AssertExpectations(t)
})
t.Run("Should search dashboards based on what is connected to a library panel", func(t *testing.T) {
mockStore.On("GetDashboardsByLibraryPanelUID", mock.Anything, "test-library-panel", int64(2)).Return([]*dashboards.DashboardRef{
{UID: "dashboard1", FolderUID: "folder1", ID: 1},
{UID: "dashboard2", FolderUID: "folder2", ID: 2},
}, nil).Once()
req := &resourcepb.ResourceSearchRequest{
Options: &resourcepb.ListOptions{
Key: dashboardKey,
Fields: []*resourcepb.Requirement{
{
Key: unisearch.DASHBOARD_LIBRARY_PANEL_REFERENCE,
Operator: "=",
Values: []string{"test-library-panel"},
},
},
},
}
resp, err := client.Search(ctx, req)
require.NoError(t, err)
require.NotNil(t, resp)
require.Equal(t, int64(2), resp.TotalHits)
require.Len(t, resp.Results.Rows, 2)
dashboardNames := make([]string, len(resp.Results.Rows))
dashboardFolders := make([]string, len(resp.Results.Rows))
dashboardIDs := make([]string, len(resp.Results.Rows))
for i, row := range resp.Results.Rows {
dashboardNames[i] = row.Key.Name
dashboardFolders[i] = string(row.Cells[1])
dashboardIDs[i] = string(row.Cells[3])
}
require.Contains(t, dashboardNames, "dashboard1")
require.Contains(t, dashboardNames, "dashboard2")
require.Contains(t, dashboardFolders, "folder1")
require.Contains(t, dashboardFolders, "folder2")
require.Contains(t, dashboardIDs, "1")
require.Contains(t, dashboardIDs, "2")
mockStore.AssertExpectations(t)
for _, row := range resp.Results.Rows {
require.Equal(t, len(row.Cells), len(resp.Results.Columns))
}
})
t.Run("Only one library panel uid is supported", func(t *testing.T) {
req := &resourcepb.ResourceSearchRequest{
Options: &resourcepb.ListOptions{
Key: dashboardKey,
Fields: []*resourcepb.Requirement{
{
Key: unisearch.DASHBOARD_LIBRARY_PANEL_REFERENCE,
Operator: "=",
Values: []string{"panel1", "panel2"},
},
},
},
}
resp, err := client.Search(ctx, req)
require.Error(t, err)
require.Contains(t, err.Error(), "only one library panel uid is supported")
require.Nil(t, resp)
})
}
func TestParseSortName(t *testing.T) {
+4
View File
@@ -191,6 +191,10 @@ func (b *DashboardsAPIBuilder) InstallSchema(scheme *runtime.Scheme) error {
return scheme.SetVersionPriority(b.GetGroupVersions()...)
}
func (b *DashboardsAPIBuilder) AllowedV0Alpha1Resources() []string {
return []string{dashv0.DashboardKind().Plural()}
}
// Validate validates dashboard operations for the apiserver
func (b *DashboardsAPIBuilder) Validate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) (err error) {
op := a.GetOperation()
@@ -120,6 +120,10 @@ func (b *SnapshotsAPIBuilder) InstallSchema(scheme *runtime.Scheme) error {
return scheme.SetVersionPriority(gv)
}
func (b *SnapshotsAPIBuilder) AllowedV0Alpha1Resources() []string {
return nil
}
func (b *SnapshotsAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, _ builder.APIGroupOptions) error {
storage := map[string]rest.Storage{}
+4
View File
@@ -201,6 +201,10 @@ func (b *DataSourceAPIBuilder) InstallSchema(scheme *runtime.Scheme) error {
return scheme.SetVersionPriority(gv)
}
func (b *DataSourceAPIBuilder) AllowedV0Alpha1Resources() []string {
return []string{builder.AllResourcesAllowed}
}
func (b *DataSourceAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error {
storage := map[string]rest.Storage{}
@@ -80,6 +80,10 @@ func (b *FeatureFlagAPIBuilder) InstallSchema(scheme *runtime.Scheme) error {
return scheme.SetVersionPriority(gv)
}
func (b *FeatureFlagAPIBuilder) AllowedV0Alpha1Resources() []string {
return nil
}
func (b *FeatureFlagAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, _ builder.APIGroupOptions) error {
featureStore := NewFeaturesStorage()
toggleStore := NewTogglesStorage(b.features)
+4
View File
@@ -130,6 +130,10 @@ func (b *FolderAPIBuilder) InstallSchema(scheme *runtime.Scheme) error {
return scheme.SetVersionPriority(b.gv)
}
func (b *FolderAPIBuilder) AllowedV0Alpha1Resources() []string {
return nil
}
func (b *FolderAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error {
scheme := opts.Scheme
optsGetter := opts.OptsGetter
+41 -6
View File
@@ -7,18 +7,53 @@ import (
"k8s.io/apiserver/pkg/authorization/authorizer"
authlib "github.com/grafana/authlib/types"
iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
"github.com/grafana/grafana/pkg/apimachinery/utils"
iamv0 "github.com/grafana/grafana/pkg/apis/iam/v0alpha1"
legacyiamv0 "github.com/grafana/grafana/pkg/apis/iam/v0alpha1"
"github.com/grafana/grafana/pkg/registry/apis/iam/legacy"
"github.com/grafana/grafana/pkg/services/accesscontrol"
gfauthorizer "github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer"
)
func newLegacyAuthorizer(ac accesscontrol.AccessControl, store legacy.LegacyIdentityStore) (authorizer.Authorizer, authlib.AccessClient) {
type iamAuthorizer struct {
resourceAuthorizer map[string]authorizer.Authorizer // Map resource to its authorizer
}
func newIAMAuthorizer(accessClient authlib.AccessClient, legacyAccessClient authlib.AccessClient) authorizer.Authorizer {
resourceAuthorizer := make(map[string]authorizer.Authorizer)
// Identity specific resources
legacyAuthorizer := gfauthorizer.NewResourceAuthorizer(legacyAccessClient)
resourceAuthorizer[legacyiamv0.UserResourceInfo.GetName()] = legacyAuthorizer
resourceAuthorizer[legacyiamv0.ServiceAccountResourceInfo.GetName()] = legacyAuthorizer
resourceAuthorizer[legacyiamv0.TeamResourceInfo.GetName()] = legacyAuthorizer
resourceAuthorizer["display"] = legacyAuthorizer
// Access specific resources
authorizer := gfauthorizer.NewResourceAuthorizer(accessClient)
resourceAuthorizer[iamv0.CoreRoleInfo.GetName()] = authorizer
return &iamAuthorizer{resourceAuthorizer: resourceAuthorizer}
}
func (s *iamAuthorizer) Authorize(ctx context.Context, attr authorizer.Attributes) (authorizer.Decision, string, error) {
if !attr.IsResourceRequest() {
return authorizer.DecisionNoOpinion, "", nil
}
authz, ok := s.resourceAuthorizer[attr.GetResource()]
if !ok {
return authorizer.DecisionDeny, "", fmt.Errorf("no authorizer found for resource %s", attr.GetResource())
}
return authz.Authorize(ctx, attr)
}
func newLegacyAccessClient(ac accesscontrol.AccessControl, store legacy.LegacyIdentityStore) authlib.AccessClient {
client := accesscontrol.NewLegacyAccessClient(
ac,
accesscontrol.ResourceAuthorizerOptions{
Resource: iamv0.UserResourceInfo.GetName(),
Resource: legacyiamv0.UserResourceInfo.GetName(),
Attr: "id",
Mapping: map[string]string{
utils.VerbGet: accesscontrol.ActionOrgUsersRead,
@@ -42,7 +77,7 @@ func newLegacyAuthorizer(ac accesscontrol.AccessControl, store legacy.LegacyIden
},
},
accesscontrol.ResourceAuthorizerOptions{
Resource: iamv0.ServiceAccountResourceInfo.GetName(),
Resource: legacyiamv0.ServiceAccountResourceInfo.GetName(),
Attr: "id",
Resolver: accesscontrol.ResourceResolverFunc(func(ctx context.Context, ns authlib.NamespaceInfo, name string) ([]string, error) {
res, err := store.GetServiceAccountInternalID(ctx, ns, legacy.GetServiceAccountInternalIDQuery{
@@ -55,7 +90,7 @@ func newLegacyAuthorizer(ac accesscontrol.AccessControl, store legacy.LegacyIden
}),
},
accesscontrol.ResourceAuthorizerOptions{
Resource: iamv0.TeamResourceInfo.GetName(),
Resource: legacyiamv0.TeamResourceInfo.GetName(),
Attr: "id",
Resolver: accesscontrol.ResourceResolverFunc(func(ctx context.Context, ns authlib.NamespaceInfo, name string) ([]string, error) {
res, err := store.GetTeamInternalID(ctx, ns, legacy.GetTeamInternalIDQuery{
@@ -69,5 +104,5 @@ func newLegacyAuthorizer(ac accesscontrol.AccessControl, store legacy.LegacyIden
},
)
return gfauthorizer.NewResourceAuthorizer(client), client
return client
}
+44
View File
@@ -0,0 +1,44 @@
package iam
import (
"github.com/grafana/authlib/types"
"github.com/grafana/grafana/pkg/registry/apis/iam/legacy"
"github.com/grafana/grafana/pkg/registry/apis/iam/user"
"github.com/grafana/grafana/pkg/services/apiserver/builder"
"github.com/grafana/grafana/pkg/services/ssosettings"
"github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/prometheus/client_golang/prometheus"
"k8s.io/apiserver/pkg/authorization/authorizer"
)
var _ builder.APIGroupBuilder = (*IdentityAccessManagementAPIBuilder)(nil)
var _ builder.APIGroupRouteProvider = (*IdentityAccessManagementAPIBuilder)(nil)
// CoreRoleStorageBackend uses the resource.StorageBackend interface to provide storage for core roles.
// Used wire to identify the storage backend for core roles.
type CoreRoleStorageBackend interface{ resource.StorageBackend }
// This is used just so wire has something unique to return
type IdentityAccessManagementAPIBuilder struct {
// Stores
store legacy.LegacyIdentityStore
coreRolesStorage CoreRoleStorageBackend
// Access Control
authorizer authorizer.Authorizer
// legacyAccessClient is used for the identity apis, we need to migrate to the access client
legacyAccessClient types.AccessClient
// accessClient is used for the core role apis
accessClient types.AccessClient
reg prometheus.Registerer
// non-k8s api route
display *user.LegacyDisplayREST
// Not set for multi-tenant deployment for now
sso ssosettings.Service
// Toggle for enabling authz management apis
enableAuthZApis bool
}
@@ -0,0 +1,56 @@
package noopstorage
import (
"context"
"errors"
"net/http"
"github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
)
var (
_ resource.StorageBackend = &StorageBackendImpl{}
errNoopStorage = errors.New("unavailable functionality")
)
type StorageBackendImpl struct{}
func ProvideStorageBackend() *StorageBackendImpl {
return &StorageBackendImpl{}
}
// GetResourceStats implements resource.StorageBackend.
func (c *StorageBackendImpl) GetResourceStats(ctx context.Context, namespace string, minCount int) ([]resource.ResourceStats, error) {
return []resource.ResourceStats{}, errNoopStorage
}
// ListHistory implements resource.StorageBackend.
func (c *StorageBackendImpl) ListHistory(context.Context, *resourcepb.ListRequest, func(resource.ListIterator) error) (int64, error) {
return 0, errNoopStorage
}
// ListIterator implements resource.StorageBackend.
func (c *StorageBackendImpl) ListIterator(context.Context, *resourcepb.ListRequest, func(resource.ListIterator) error) (int64, error) {
return 0, errNoopStorage
}
// ReadResource implements resource.StorageBackend.
func (c *StorageBackendImpl) ReadResource(_ context.Context, req *resourcepb.ReadRequest) *resource.BackendReadResponse {
return &resource.BackendReadResponse{
Key: req.GetKey(),
Error: &resourcepb.ErrorResult{Code: http.StatusForbidden, Message: errNoopStorage.Error()},
}
}
// WatchWriteEvents implements resource.StorageBackend.
func (c *StorageBackendImpl) WatchWriteEvents(ctx context.Context) (<-chan *resource.WrittenEvent, error) {
stream := make(chan *resource.WrittenEvent, 10)
return stream, nil
}
// WriteEvent implements resource.StorageBackend.
func (c *StorageBackendImpl) WriteEvent(context.Context, resource.WriteEvent) (int64, error) {
return 0, errNoopStorage
}
+93 -39
View File
@@ -4,10 +4,12 @@ import (
"context"
"strings"
"github.com/prometheus/client_golang/prometheus"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/registry/generic"
"k8s.io/apiserver/pkg/registry/rest"
genericapiserver "k8s.io/apiserver/pkg/server"
common "k8s.io/kube-openapi/pkg/common"
@@ -15,8 +17,12 @@ import (
"k8s.io/kube-openapi/pkg/validation/spec"
"github.com/grafana/authlib/types"
iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
"github.com/grafana/grafana/pkg/apimachinery/identity"
iamv0 "github.com/grafana/grafana/pkg/apis/iam/v0alpha1"
"github.com/grafana/grafana/pkg/apimachinery/utils"
legacyiamv0 "github.com/grafana/grafana/pkg/apis/iam/v0alpha1"
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/registry/apis/iam/legacy"
"github.com/grafana/grafana/pkg/registry/apis/iam/serviceaccount"
@@ -25,41 +31,37 @@ import (
"github.com/grafana/grafana/pkg/registry/apis/iam/user"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/apiserver/builder"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/ssosettings"
"github.com/grafana/grafana/pkg/storage/legacysql"
"github.com/grafana/grafana/pkg/storage/unified/apistore"
"github.com/grafana/grafana/pkg/storage/unified/resource"
)
var _ builder.APIGroupBuilder = (*IdentityAccessManagementAPIBuilder)(nil)
var _ builder.APIGroupRouteProvider = (*IdentityAccessManagementAPIBuilder)(nil)
// This is used just so wire has something unique to return
type IdentityAccessManagementAPIBuilder struct {
store legacy.LegacyIdentityStore
authorizer authorizer.Authorizer
accessClient types.AccessClient
// non-k8s api route
display *user.LegacyDisplayREST
// Not set for multi-tenant deployment for now
sso ssosettings.Service
}
func RegisterAPIService(
features featuremgmt.FeatureToggles,
apiregistration builder.APIRegistrar,
ssoService ssosettings.Service,
sql db.DB,
ac accesscontrol.AccessControl,
accessClient types.AccessClient,
reg prometheus.Registerer,
coreRolesStorage CoreRoleStorageBackend,
) (*IdentityAccessManagementAPIBuilder, error) {
store := legacy.NewLegacySQLStores(legacysql.NewDatabaseProvider(sql))
authorizer, client := newLegacyAuthorizer(ac, store)
legacyAccessClient := newLegacyAccessClient(ac, store)
authorizer := newIAMAuthorizer(accessClient, legacyAccessClient)
builder := &IdentityAccessManagementAPIBuilder{
store: store,
sso: ssoService,
authorizer: authorizer,
accessClient: client,
display: user.NewLegacyDisplayREST(store),
store: store,
coreRolesStorage: coreRolesStorage,
sso: ssoService,
authorizer: authorizer,
legacyAccessClient: legacyAccessClient,
accessClient: accessClient,
display: user.NewLegacyDisplayREST(store),
reg: reg,
enableAuthZApis: features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthzApis),
}
apiregistration.RegisterAPI(builder)
@@ -85,50 +87,80 @@ func NewAPIService(store legacy.LegacyIdentityStore) *IdentityAccessManagementAP
}
func (b *IdentityAccessManagementAPIBuilder) GetGroupVersion() schema.GroupVersion {
return iamv0.SchemeGroupVersion
return legacyiamv0.SchemeGroupVersion
}
func (b *IdentityAccessManagementAPIBuilder) InstallSchema(scheme *runtime.Scheme) error {
iamv0.AddKnownTypes(scheme, iamv0.VERSION)
if b.enableAuthZApis {
if err := iamv0.AddToScheme(scheme); err != nil {
return err
}
}
legacyiamv0.AddKnownTypes(scheme, legacyiamv0.VERSION)
// Link this version to the internal representation.
// This is used for server-side-apply (PATCH), and avoids the error:
// "no kind is registered for the type"
iamv0.AddKnownTypes(scheme, runtime.APIVersionInternal)
legacyiamv0.AddKnownTypes(scheme, runtime.APIVersionInternal)
metav1.AddToGroupVersion(scheme, iamv0.SchemeGroupVersion)
return scheme.SetVersionPriority(iamv0.SchemeGroupVersion)
metav1.AddToGroupVersion(scheme, legacyiamv0.SchemeGroupVersion)
return scheme.SetVersionPriority(legacyiamv0.SchemeGroupVersion)
}
func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, _ builder.APIGroupOptions) error {
func (b *IdentityAccessManagementAPIBuilder) AllowedV0Alpha1Resources() []string {
return []string{builder.AllResourcesAllowed}
}
func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error {
storage := map[string]rest.Storage{}
teamResource := iamv0.TeamResourceInfo
storage[teamResource.StoragePath()] = team.NewLegacyStore(b.store, b.accessClient)
teamResource := legacyiamv0.TeamResourceInfo
storage[teamResource.StoragePath()] = team.NewLegacyStore(b.store, b.legacyAccessClient)
storage[teamResource.StoragePath("members")] = team.NewLegacyTeamMemberREST(b.store)
teamBindingResource := iamv0.TeamBindingResourceInfo
teamBindingResource := legacyiamv0.TeamBindingResourceInfo
storage[teamBindingResource.StoragePath()] = team.NewLegacyBindingStore(b.store)
userResource := iamv0.UserResourceInfo
storage[userResource.StoragePath()] = user.NewLegacyStore(b.store, b.accessClient)
userResource := legacyiamv0.UserResourceInfo
storage[userResource.StoragePath()] = user.NewLegacyStore(b.store, b.legacyAccessClient)
storage[userResource.StoragePath("teams")] = user.NewLegacyTeamMemberREST(b.store)
serviceAccountResource := iamv0.ServiceAccountResourceInfo
storage[serviceAccountResource.StoragePath()] = serviceaccount.NewLegacyStore(b.store, b.accessClient)
serviceAccountResource := legacyiamv0.ServiceAccountResourceInfo
storage[serviceAccountResource.StoragePath()] = serviceaccount.NewLegacyStore(b.store, b.legacyAccessClient)
storage[serviceAccountResource.StoragePath("tokens")] = serviceaccount.NewLegacyTokenREST(b.store)
if b.sso != nil {
ssoResource := iamv0.SSOSettingResourceInfo
ssoResource := legacyiamv0.SSOSettingResourceInfo
storage[ssoResource.StoragePath()] = sso.NewLegacyStore(b.sso)
}
apiGroupInfo.VersionedResourcesStorageMap[iamv0.VERSION] = storage
if b.enableAuthZApis {
// v0alpha1
store, err := NewLocalStore(iamv0.CoreRoleInfo, apiGroupInfo.Scheme, opts.OptsGetter, b.reg, b.accessClient, b.coreRolesStorage)
if err != nil {
return err
}
storage[iamv0.CoreRoleInfo.StoragePath()] = store
}
apiGroupInfo.VersionedResourcesStorageMap[legacyiamv0.VERSION] = storage
return nil
}
func (b *IdentityAccessManagementAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions {
return iamv0.GetOpenAPIDefinitions
defs := legacyiamv0.GetOpenAPIDefinitions
if b.enableAuthZApis {
defs = func(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
def1 := legacyiamv0.GetOpenAPIDefinitions(ref)
def2 := iamv0.GetOpenAPIDefinitions(ref)
for k, v := range def2 {
def1[k] = v
}
return def1
}
}
return defs
}
func (b *IdentityAccessManagementAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.OpenAPI, error) {
@@ -191,3 +223,25 @@ func (b *IdentityAccessManagementAPIBuilder) GetAPIRoutes(gv schema.GroupVersion
func (b *IdentityAccessManagementAPIBuilder) GetAuthorizer() authorizer.Authorizer {
return b.authorizer
}
func NewLocalStore(resourceInfo utils.ResourceInfo, scheme *runtime.Scheme, defaultOptsGetter generic.RESTOptionsGetter,
reg prometheus.Registerer, ac types.AccessClient, storageBackend resource.StorageBackend) (grafanarest.Storage, error) {
server, err := resource.NewResourceServer(resource.ResourceServerOptions{
Backend: storageBackend,
Reg: reg,
AccessClient: ac,
})
if err != nil {
return nil, err
}
defaultOpts, err := defaultOptsGetter.GetRESTOptions(resourceInfo.GroupResource(), nil)
if err != nil {
return nil, err
}
client := resource.NewLocalResourceClient(server)
optsGetter := apistore.NewRESTOptionsGetterForClient(client, defaultOpts.StorageConfig.Config, nil)
store, err := grafanaregistry.NewRegistryStore(scheme, resourceInfo, optsGetter)
return store, err
}
+109
View File
@@ -0,0 +1,109 @@
package ofrep
import (
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"net/http"
"net/http/httputil"
"os"
"path"
"github.com/grafana/grafana/pkg/util/proxyutil"
)
func (b *APIBuilder) proxyAllFlagReq(isAuthedUser bool, w http.ResponseWriter, r *http.Request) {
proxy, err := b.newProxy(ofrepPath)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
proxy.ModifyResponse = func(resp *http.Response) error {
if resp.StatusCode == http.StatusOK && !isAuthedUser {
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return err
}
_ = resp.Body.Close()
filtered := make(map[string]any)
for k, v := range result {
if isPublicFlag(k) {
filtered[k] = v
}
}
writeResponse(http.StatusOK, filtered, b.logger, w)
}
return nil
}
proxy.ServeHTTP(w, r)
}
func (b *APIBuilder) proxyFlagReq(flagKey string, isAuthedUser bool, w http.ResponseWriter, r *http.Request) {
proxy, err := b.newProxy(path.Join(ofrepPath, flagKey))
if err != nil {
b.logger.Error("Failed to create proxy", "key", flagKey, "error", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
proxy.ModifyResponse = func(resp *http.Response) error {
if resp.StatusCode == http.StatusOK && !isAuthedUser && !isPublicFlag(flagKey) {
writeResponse(http.StatusUnauthorized, struct{}{}, b.logger, w)
}
return nil
}
proxy.ServeHTTP(w, r)
}
func (b *APIBuilder) newProxy(proxyPath string) (*httputil.ReverseProxy, error) {
if proxyPath == "" {
return nil, fmt.Errorf("proxy path is required")
}
if b.url == nil {
return nil, fmt.Errorf("OpenFeatureService provider URL is not set")
}
var caRoot *x509.CertPool
if b.caFile != "" {
var err error
caRoot, err = getCARoot(b.caFile)
if err != nil {
return nil, err
}
}
director := func(req *http.Request) {
req.URL.Scheme = b.url.Scheme
req.URL.Host = b.url.Host
req.URL.Path = proxyPath
}
proxy := proxyutil.NewReverseProxy(b.logger, director)
proxy.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: b.insecure,
RootCAs: caRoot,
},
}
return proxy, nil
}
func getCARoot(caFile string) (*x509.CertPool, error) {
// It should be safe to ignore since caFile is passed as --internal.root-ca-file flag of apiserver
// nolint:gosec
caCert, err := os.ReadFile(caFile)
if err != nil {
return nil, err
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
return caCertPool, nil
}
+11
View File
@@ -0,0 +1,11 @@
package ofrep
// publicFlags contains the list of flags that can be evaluated by unauthenticated users
var publicFlags = map[string]bool{
"testflag": true,
}
func isPublicFlag(flagKey string) bool {
_, exists := publicFlags[flagKey]
return exists
}
+234
View File
@@ -0,0 +1,234 @@
package ofrep
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/grafana/authlib/types"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/authorization/authorizer"
genericapiserver "k8s.io/apiserver/pkg/server"
"k8s.io/kube-openapi/pkg/common"
"k8s.io/kube-openapi/pkg/spec3"
"github.com/gorilla/mux"
"github.com/grafana/grafana/pkg/services/apiserver/builder"
)
var _ builder.APIGroupBuilder = (*APIBuilder)(nil)
var _ builder.APIGroupRouteProvider = (*APIBuilder)(nil)
var _ builder.APIGroupVersionProvider = (*APIBuilder)(nil)
const ofrepPath = "/ofrep/v1/evaluate/flags"
type APIBuilder struct {
providerType string
url *url.URL
insecure bool
caFile string
staticEvaluator featuremgmt.StaticFlagEvaluator
logger log.Logger
}
func NewAPIBuilder(providerType string, url *url.URL, insecure bool, caFile string, staticEvaluator featuremgmt.StaticFlagEvaluator) *APIBuilder {
return &APIBuilder{
providerType: providerType,
url: url,
insecure: insecure,
caFile: caFile,
staticEvaluator: staticEvaluator,
logger: log.New("grafana-apiserver.feature-flags"),
}
}
func RegisterAPIService(apiregistration builder.APIRegistrar, cfg *setting.Cfg, staticEvaluator featuremgmt.StaticFlagEvaluator) *APIBuilder {
b := NewAPIBuilder(cfg.OpenFeature.ProviderType, cfg.OpenFeature.URL, true, "", staticEvaluator)
apiregistration.RegisterAPI(b)
return b
}
func (b *APIBuilder) GetAuthorizer() authorizer.Authorizer {
return authorizer.AuthorizerFunc(func(ctx context.Context, attr authorizer.Attributes) (authorizer.Decision, string, error) {
// Allow all requests - we'll handle auth in the handler
return authorizer.DecisionAllow, "", nil
})
}
func (b *APIBuilder) GetGroupVersion() schema.GroupVersion {
return schema.GroupVersion{
Group: "features.grafana.app",
Version: "v0alpha1",
}
}
func (b *APIBuilder) InstallSchema(scheme *runtime.Scheme) error {
metav1.AddToGroupVersion(scheme, b.GetGroupVersion())
return scheme.SetVersionPriority(b.GetGroupVersion())
}
func (b *APIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error {
return nil
}
func (b *APIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions {
return func(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
return map[string]common.OpenAPIDefinition{}
}
}
func (b *APIBuilder) AllowedV0Alpha1Resources() []string {
return []string{builder.AllResourcesAllowed}
}
func (b *APIBuilder) GetAPIRoutes(gv schema.GroupVersion) *builder.APIRoutes {
return &builder.APIRoutes{
Namespace: []builder.APIRouteHandler{
{
Path: "ofrep/v1/evaluate/flags/",
Spec: &spec3.PathProps{
Post: &spec3.Operation{},
},
Handler: b.allFlagsHandler,
},
{
Path: "ofrep/v1/evaluate/flags/{flagKey}",
Spec: &spec3.PathProps{
Post: &spec3.Operation{},
},
Handler: b.oneFlagHandler,
},
},
}
}
func (b *APIBuilder) oneFlagHandler(w http.ResponseWriter, r *http.Request) {
if !b.validateNamespace(r) {
b.logger.Error("stackId in evaluation context does not match requested namespace")
http.Error(w, "stackId in evaluation context does not match requested namespace", http.StatusUnauthorized)
return
}
flagKey := mux.Vars(r)["flagKey"]
if flagKey == "" {
http.Error(w, "flagKey parameter is required", http.StatusBadRequest)
return
}
isAuthedReq := b.isAuthenticatedRequest(r)
// Unless the request is authenticated, we only allow public flags evaluations
if !isAuthedReq && !isPublicFlag(flagKey) {
b.logger.Error("Unauthorized to evaluate flag", "flagKey", flagKey)
http.Error(w, "unauthorized to evaluate flag", http.StatusUnauthorized)
return
}
if b.providerType == setting.GOFFProviderType {
b.proxyFlagReq(flagKey, isAuthedReq, w, r)
return
}
b.evalFlagStatic(flagKey, w, r)
}
func (b *APIBuilder) allFlagsHandler(w http.ResponseWriter, r *http.Request) {
if !b.validateNamespace(r) {
b.logger.Error("stackId in evaluation context does not match requested namespace")
http.Error(w, "stackId in evaluation context does not match requested namespace", http.StatusUnauthorized)
return
}
isAuthedReq := b.isAuthenticatedRequest(r)
if b.providerType == setting.GOFFProviderType {
b.proxyAllFlagReq(isAuthedReq, w, r)
return
}
b.evalAllFlagsStatic(isAuthedReq, w, r)
}
func writeResponse(statusCode int, result any, logger log.Logger, w http.ResponseWriter) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
if err := json.NewEncoder(w).Encode(result); err != nil {
logger.Error("Failed to encode flag evaluation result", "error", err)
}
}
func (b *APIBuilder) stackIdFromEvalCtx(body []byte) string {
// Extract stackID from request body without consuming it
var evalCtx struct {
Context struct {
StackID int32 `json:"stackId"`
} `json:"context"`
}
if err := json.Unmarshal(body, &evalCtx); err != nil {
b.logger.Debug("Failed to unmarshal evaluation context", "error", err, "body", string(body))
return ""
}
if evalCtx.Context.StackID <= 0 {
b.logger.Debug("Invalid or missing stackId in evaluation context", "stackId", evalCtx.Context.StackID)
return ""
}
return strconv.Itoa(int(evalCtx.Context.StackID))
}
func removeStackPrefix(tenant string) string {
return strings.TrimPrefix(tenant, "stacks-")
}
// isAuthenticatedRequest returns true if the request is authenticated
func (b *APIBuilder) isAuthenticatedRequest(r *http.Request) bool {
user, ok := types.AuthInfoFrom(r.Context())
if !ok {
return false
}
return user.GetIdentityType() != ""
}
// validateNamespace checks if the stackId in the evaluation context matches the namespace in the request
func (b *APIBuilder) validateNamespace(r *http.Request) bool {
// Extract namespace from request context or URL path
var namespace string
user, ok := types.AuthInfoFrom(r.Context())
if !ok {
return false
}
if user.GetNamespace() != "" {
namespace = user.GetNamespace()
} else {
namespace = mux.Vars(r)["namespace"]
}
// Extract stackId from feature flag evaluation context
body, err := io.ReadAll(r.Body)
if err != nil {
b.logger.Error("Error reading evaluation request body", "error", err)
return false
}
r.Body = io.NopCloser(bytes.NewBuffer(body))
// "default" namespace case can only occur in on-prem grafana
if b.stackIdFromEvalCtx(body) == removeStackPrefix(namespace) || namespace == "default" {
return true
}
return false
}
+41
View File
@@ -0,0 +1,41 @@
package ofrep
import (
"net/http"
"github.com/grafana/grafana/pkg/services/featuremgmt"
)
func (b *APIBuilder) evalAllFlagsStatic(isAuthedUser bool, w http.ResponseWriter, r *http.Request) {
result, err := b.staticEvaluator.EvalAllFlags(r.Context())
if err != nil {
b.logger.Error("Failed to evaluate all static flags", "error", err)
http.Error(w, "failed to evaluate flags", http.StatusInternalServerError)
return
}
if !isAuthedUser {
var publicOnly []featuremgmt.OFREPFlag
for _, flag := range result.Flags {
if isPublicFlag(flag.Key) {
publicOnly = append(publicOnly, flag)
}
}
result.Flags = publicOnly
}
writeResponse(http.StatusOK, result, b.logger, w)
}
func (b *APIBuilder) evalFlagStatic(flagKey string, w http.ResponseWriter, r *http.Request) {
result, err := b.staticEvaluator.EvalFlag(r.Context(), flagKey)
if err != nil {
b.logger.Error("Failed to evaluate static flag", "key", flagKey, "error", err)
http.Error(w, "failed to evaluate flag", http.StatusInternalServerError)
return
}
writeResponse(http.StatusOK, result, b.logger, w)
}
@@ -321,6 +321,10 @@ func (b *APIBuilder) InstallSchema(scheme *runtime.Scheme) error {
return scheme.SetVersionPriority(provisioning.SchemeGroupVersion)
}
func (b *APIBuilder) AllowedV0Alpha1Resources() []string {
return []string{builder.AllResourcesAllowed}
}
func (b *APIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error {
repositoryStorage, err := grafanaregistry.NewRegistryStore(opts.Scheme, provisioning.RepositoryResourceInfo, opts.OptsGetter)
if err != nil {
@@ -161,7 +161,8 @@ func (r *gitRepository) Test(ctx context.Context) (*provisioning.TestResults, er
Type: metav1.CauseTypeFieldValueInvalid,
Field: field.NewPath("spec", t, "token").String(),
Detail: detail,
}}}, nil
}},
}, nil
}
if ok, err := r.client.RepoExists(ctx); err != nil || !ok {
@@ -177,7 +178,8 @@ func (r *gitRepository) Test(ctx context.Context) (*provisioning.TestResults, er
Type: metav1.CauseTypeFieldValueInvalid,
Field: field.NewPath("spec", t, "url").String(),
Detail: detail,
}}}, nil
}},
}, nil
}
// Test basic connectivity by getting the branch reference
@@ -192,7 +194,8 @@ func (r *gitRepository) Test(ctx context.Context) (*provisioning.TestResults, er
Type: metav1.CauseTypeFieldValueInvalid,
Field: field.NewPath("spec", t, "branch").String(),
Detail: detail,
}}}, nil
}},
}, nil
}
detail = fmt.Sprintf("failed to check if branch exists: %v", err)
@@ -204,7 +207,8 @@ func (r *gitRepository) Test(ctx context.Context) (*provisioning.TestResults, er
Type: metav1.CauseTypeFieldValueInvalid,
Field: field.NewPath("spec", t, "branch").String(),
Detail: detail,
}}}, nil
}},
}, nil
}
return &provisioning.TestResults{
@@ -186,8 +186,6 @@ func (r *stagedGitRepository) Push(ctx context.Context, opts repository.PushOpti
return r.writer.Push(ctx)
}
func (r *stagedGitRepository) Remove(_ context.Context) error {
// Since we're using nanogit which doesn't actually clone the repository,
// we don't need to do anything here as there's nothing to clean up
return nil
func (r *stagedGitRepository) Remove(ctx context.Context) error {
return r.writer.Cleanup(ctx)
}
@@ -895,9 +895,8 @@ func TestStagedGitRepository_Remove(t *testing.T) {
stagedRepo := createTestStagedRepositoryWithWriter(mockWriter, repository.CloneOptions{})
err := stagedRepo.Remove(context.Background())
require.NoError(t, err)
// No mock calls expected since Remove is a no-op
require.Equal(t, 1, mockWriter.CleanupCallCount())
})
}
+85
View File
@@ -67,6 +67,91 @@ func TestQuerySplitting(t *testing.T) {
require.Equal(t, "0", split.Requests[0].Request.From)
require.Equal(t, "0", split.Requests[0].Request.To)
})
t.Run("forbid duplicate refId", func(t *testing.T) {
_, err := parser.parseRequest(ctx, &query.QueryDataRequest{
QueryDataRequest: data.QueryDataRequest{
TimeRange: data.TimeRange{},
Queries: []data.DataQuery{
{
CommonQueryProperties: data.CommonQueryProperties{
RefID: "A",
Datasource: &data.DataSourceRef{
Type: "x",
UID: "abc",
},
},
},
{
CommonQueryProperties: data.CommonQueryProperties{
RefID: "A",
Datasource: &data.DataSourceRef{
Type: "x",
UID: "abc",
},
},
},
},
},
})
require.Error(t, err)
})
t.Run("forbid duplicate refId, when refId=''", func(t *testing.T) {
_, err := parser.parseRequest(ctx, &query.QueryDataRequest{
QueryDataRequest: data.QueryDataRequest{
TimeRange: data.TimeRange{},
Queries: []data.DataQuery{
{
CommonQueryProperties: data.CommonQueryProperties{
RefID: "",
Datasource: &data.DataSourceRef{
Type: "x",
UID: "abc",
},
},
},
{
CommonQueryProperties: data.CommonQueryProperties{
RefID: "",
Datasource: &data.DataSourceRef{
Type: "x",
UID: "abc",
},
},
},
},
},
})
require.Error(t, err)
})
t.Run("allow empty refId", func(t *testing.T) {
_, err := parser.parseRequest(ctx, &query.QueryDataRequest{
QueryDataRequest: data.QueryDataRequest{
TimeRange: data.TimeRange{},
Queries: []data.DataQuery{
{
CommonQueryProperties: data.CommonQueryProperties{
RefID: "",
Datasource: &data.DataSourceRef{
Type: "x",
UID: "abc",
},
},
},
{
CommonQueryProperties: data.CommonQueryProperties{
RefID: "B",
Datasource: &data.DataSourceRef{
Type: "x",
UID: "abc",
},
},
},
},
},
})
require.NoError(t, err)
})
t.Run("applies query time range if present", func(t *testing.T) {
split, err := parser.parseRequest(ctx, &query.QueryDataRequest{
QueryDataRequest: data.QueryDataRequest{
+4
View File
@@ -159,6 +159,10 @@ func (b *QueryAPIBuilder) InstallSchema(scheme *runtime.Scheme) error {
return scheme.SetVersionPriority(query.SchemeGroupVersion)
}
func (b *QueryAPIBuilder) AllowedV0Alpha1Resources() []string {
return []string{builder.AllResourcesAllowed}
}
func (b *QueryAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, _ builder.APIGroupOptions) error {
gv := query.SchemeGroupVersion
+4
View File
@@ -127,6 +127,10 @@ func (b *SecretAPIBuilder) InstallSchema(scheme *runtime.Scheme) error {
return nil
}
func (b *SecretAPIBuilder) AllowedV0Alpha1Resources() []string {
return nil
}
// UpdateAPIGroupInfo is called when creating a generic API server for this group of kinds.
func (b *SecretAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error {
secureValueResource := secretv0alpha1.SecureValuesResourceInfo
+4
View File
@@ -68,6 +68,10 @@ func (b *ServiceAPIBuilder) InstallSchema(scheme *runtime.Scheme) error {
return scheme.SetVersionPriority(gv)
}
func (b *ServiceAPIBuilder) AllowedV0Alpha1Resources() []string {
return []string{builder.AllResourcesAllowed}
}
func (b *ServiceAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error {
scheme := opts.Scheme
optsGetter := opts.OptsGetter
@@ -54,6 +54,10 @@ func (b *UserStorageAPIBuilder) InstallSchema(scheme *runtime.Scheme) error {
return scheme.SetVersionPriority(gv)
}
func (b *UserStorageAPIBuilder) AllowedV0Alpha1Resources() []string {
return []string{builder.AllResourcesAllowed}
}
func (b *UserStorageAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error {
resourceInfo := userstorage.UserStorageResourceInfo
storage := map[string]rest.Storage{}
+10
View File
@@ -9,6 +9,8 @@ import (
"github.com/grafana/grafana/pkg/registry/apis/featuretoggle"
"github.com/grafana/grafana/pkg/registry/apis/folders"
"github.com/grafana/grafana/pkg/registry/apis/iam"
"github.com/grafana/grafana/pkg/registry/apis/iam/noopstorage"
"github.com/grafana/grafana/pkg/registry/apis/ofrep"
"github.com/grafana/grafana/pkg/registry/apis/provisioning"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/webhooks"
"github.com/grafana/grafana/pkg/registry/apis/query"
@@ -30,6 +32,12 @@ var ProvisioningExtras = wire.NewSet(
MergeProvisioningExtras,
)
// WireSetExts is a set of providers that can be overridden by enterprise implementations.
var WireSetExts = wire.NewSet(
noopstorage.ProvideStorageBackend,
wire.Bind(new(iam.CoreRoleStorageBackend), new(*noopstorage.StorageBackendImpl)),
)
var WireSet = wire.NewSet(
ProvideRegistryServiceSink, // dummy background service that forces registration
@@ -44,6 +52,7 @@ var WireSet = wire.NewSet(
featuretoggle.RegisterAPIService,
datasource.RegisterAPIService,
folders.RegisterAPIService,
WireSetExts, // this will be moved to wireexts_oss.go in a following PR
iam.RegisterAPIService,
ProvisioningExtras,
provisioning.RegisterAPIService,
@@ -51,4 +60,5 @@ var WireSet = wire.NewSet(
query.RegisterAPIService,
secret.RegisterAPIService,
userstorage.RegisterAPIService,
ofrep.RegisterAPIService,
)
+6 -4
View File
@@ -7,6 +7,7 @@ import (
advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1"
advisorapp "github.com/grafana/grafana/apps/advisor/pkg/app"
"github.com/grafana/grafana/apps/advisor/pkg/app/checkregistry"
"github.com/grafana/grafana/pkg/services/apiserver/builder"
"github.com/grafana/grafana/pkg/services/apiserver/builder/runner"
"github.com/grafana/grafana/pkg/setting"
)
@@ -27,10 +28,11 @@ func RegisterApp(
StackID: cfg.StackID,
}
appCfg := &runner.AppBuilderConfig{
OpenAPIDefGetter: advisorv0alpha1.GetOpenAPIDefinitions,
ManagedKinds: advisorapp.GetKinds(),
Authorizer: advisorapp.GetAuthorizer(),
CustomConfig: any(specificConfig),
OpenAPIDefGetter: advisorv0alpha1.GetOpenAPIDefinitions,
ManagedKinds: advisorapp.GetKinds(),
Authorizer: advisorapp.GetAuthorizer(),
CustomConfig: any(specificConfig),
AllowedV0Alpha1Resources: []string{builder.AllResourcesAllowed},
}
provider.Provider = simple.NewAppProvider(apis.LocalManifest(), appCfg, advisorapp.New)
return provider
@@ -17,6 +17,7 @@ import (
"github.com/grafana/grafana/pkg/registry/apps/alerting/notifications/templategroup"
"github.com/grafana/grafana/pkg/registry/apps/alerting/notifications/timeinterval"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/apiserver/builder"
"github.com/grafana/grafana/pkg/services/apiserver/builder/runner"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/ngalert"
@@ -37,10 +38,11 @@ func RegisterApp(
return nil
}
appCfg := &runner.AppBuilderConfig{
Authorizer: getAuthorizer(ng.Api.AccessControl),
LegacyStorageGetter: getLegacyStorage(request.GetNamespaceMapper(cfg), ng),
OpenAPIDefGetter: v0alpha1.GetOpenAPIDefinitions,
ManagedKinds: notificationsResource.GetKinds(),
Authorizer: getAuthorizer(ng.Api.AccessControl),
LegacyStorageGetter: getLegacyStorage(request.GetNamespaceMapper(cfg), ng),
OpenAPIDefGetter: v0alpha1.GetOpenAPIDefinitions,
ManagedKinds: notificationsResource.GetKinds(),
AllowedV0Alpha1Resources: []string{builder.AllResourcesAllowed},
}
return &AlertingNotificationsAppProvider{
+5 -3
View File
@@ -6,6 +6,7 @@ import (
"github.com/grafana/grafana/apps/investigations/pkg/apis"
investigationv0alpha1 "github.com/grafana/grafana/apps/investigations/pkg/apis/investigations/v0alpha1"
investigationapp "github.com/grafana/grafana/apps/investigations/pkg/app"
"github.com/grafana/grafana/pkg/services/apiserver/builder"
"github.com/grafana/grafana/pkg/services/apiserver/builder/runner"
"github.com/grafana/grafana/pkg/setting"
)
@@ -22,9 +23,10 @@ func RegisterApp(
cfg: cfg,
}
appCfg := &runner.AppBuilderConfig{
OpenAPIDefGetter: investigationv0alpha1.GetOpenAPIDefinitions,
ManagedKinds: investigationapp.GetKinds(),
Authorizer: investigationapp.GetAuthorizer(),
OpenAPIDefGetter: investigationv0alpha1.GetOpenAPIDefinitions,
ManagedKinds: investigationapp.GetKinds(),
Authorizer: investigationapp.GetAuthorizer(),
AllowedV0Alpha1Resources: []string{builder.AllResourcesAllowed},
}
provider.Provider = simple.NewAppProvider(apis.LocalManifest(), appCfg, investigationapp.New)
return provider
+1
View File
@@ -43,6 +43,7 @@ func RegisterApp(
CustomConfig: any(&playlistapp.PlaylistConfig{
EnableReconcilers: features.IsEnabledGlobally(featuremgmt.FlagPlaylistsReconciler),
}),
AllowedV0Alpha1Resources: []string{playlistv0alpha1.PlaylistKind().Plural()},
}
provider.Provider = simple.NewAppProvider(apis.LocalManifest(), appCfg, playlistapp.New)
return provider
+6 -15
View File
@@ -6,26 +6,17 @@ import (
"github.com/grafana/dskit/services"
"github.com/grafana/grafana/pkg/modules"
"github.com/grafana/grafana/pkg/services/grpcserver"
"github.com/grafana/grafana/pkg/services/grpcserver/interceptors"
"github.com/grafana/grafana/pkg/storage/unified/resource"
resourcegrpc "github.com/grafana/grafana/pkg/storage/unified/resource/grpc"
"github.com/grafana/grafana/pkg/storage/unified/sql"
"go.opentelemetry.io/otel"
)
func (ms *ModuleServer) initDistributor() (services.Service, error) {
distributor := &distributorService{}
tracer := otel.Tracer("unified-storage-distributor")
// FIXME: This is a temporary solution while we are migrating to the new authn interceptor
// grpcutils.NewGrpcAuthenticator should be used instead.
authn := sql.NewAuthenticatorWithFallback(ms.cfg, ms.registerer, tracer, func(ctx context.Context) (context.Context, error) {
auth := resourcegrpc.Authenticator{Tracer: tracer}
return auth.Authenticate(ctx)
})
var err error
distributor.grpcHandler, err = resource.ProvideDistributorServer(ms.cfg, ms.features, interceptors.AuthenticatorFunc(authn), ms.registerer, tracer, ms.storageRing, ms.storageRingClientPool)
var (
distributor = &distributorService{}
tracer = otel.Tracer("unified-storage-distributor")
err error
)
distributor.grpcHandler, err = resource.ProvideDistributorServer(ms.cfg, ms.features, ms.registerer, tracer, ms.storageRing, ms.storageRingClientPool)
if err != nil {
return nil, err
}
+2 -1
View File
@@ -16,6 +16,7 @@ import (
"github.com/prometheus/client_golang/prometheus"
"github.com/grafana/dskit/services"
"github.com/grafana/grafana/pkg/api"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/modules"
@@ -173,7 +174,7 @@ func (s *ModuleServer) Run() error {
if err != nil {
return nil, err
}
return sql.ProvideUnifiedStorageGrpcService(s.cfg, s.features, nil, s.log, nil, docBuilders, s.storageMetrics, s.indexMetrics, s.storageRing, s.MemberlistKVConfig)
return sql.ProvideUnifiedStorageGrpcService(s.cfg, s.features, nil, s.log, s.registerer, docBuilders, s.storageMetrics, s.indexMetrics, s.storageRing, s.MemberlistKVConfig)
})
m.RegisterModule(modules.ZanzanaServer, func() (services.Service, error) {
+1 -4
View File
@@ -124,10 +124,7 @@ func newClientPool(clientCfg grpcclient.Config, log log.Logger, reg prometheus.R
return nil, fmt.Errorf("failed to dial resource server %s %s: %s", inst.Id, inst.Addr, err)
}
client, err := resource.NewResourceClient(conn, cfg, features, tracer)
if err != nil {
return nil, fmt.Errorf("failed to get resource client for server %s %s: %s", inst.Id, inst.Addr, err)
}
client := resource.NewAuthlessResourceClient(conn)
return &resource.RingClient{
Client: client,
+1
View File
@@ -324,6 +324,7 @@ var wireBasicSet = wire.NewSet(
featuremgmt.ProvideManagerService,
featuremgmt.ProvideToggles,
featuremgmt.ProvideOpenFeatureService,
featuremgmt.ProvideStaticEvaluator,
dashboardservice.ProvideDashboardServiceImpl,
wire.Bind(new(dashboards.PermissionsRegistrationService), new(*dashboardservice.DashboardServiceImpl)),
dashboardservice.ProvideDashboardService,
@@ -72,35 +72,48 @@ func folderTreeCollector(folderService folder.Service) legacyTupleCollector {
ctx, ident := identity.WithServiceIdentity(ctx, orgID)
q := folder.GetFoldersQuery{
OrgID: orgID,
SignedInUser: ident,
}
folders, err := folderService.GetFolders(ctx, q)
if err != nil {
return nil, err
}
tuples := make(map[string]map[string]*openfgav1.TupleKey)
for _, f := range folders {
var tuple *openfgav1.TupleKey
if f.ParentUID == "" {
continue
const pageSize = 1000
var page int64 = 1
for {
q := folder.GetFoldersQuery{
OrgID: orgID,
SignedInUser: ident,
Limit: pageSize,
Page: page,
}
tuple = &openfgav1.TupleKey{
Object: zanzana.NewTupleEntry(zanzana.TypeFolder, f.UID, ""),
Relation: zanzana.RelationParent,
User: zanzana.NewTupleEntry(zanzana.TypeFolder, f.ParentUID, ""),
folders, err := folderService.GetFolders(ctx, q)
if err != nil {
return nil, err
}
if tuples[tuple.Object] == nil {
tuples[tuple.Object] = make(map[string]*openfgav1.TupleKey)
for _, f := range folders {
var tuple *openfgav1.TupleKey
if f.ParentUID == "" {
continue
}
tuple = &openfgav1.TupleKey{
Object: zanzana.NewTupleEntry(zanzana.TypeFolder, f.UID, ""),
Relation: zanzana.RelationParent,
User: zanzana.NewTupleEntry(zanzana.TypeFolder, f.ParentUID, ""),
}
if tuples[tuple.Object] == nil {
tuples[tuple.Object] = make(map[string]*openfgav1.TupleKey)
}
tuples[tuple.Object][tuple.String()] = tuple
}
tuples[tuple.Object][tuple.String()] = tuple
if int64(len(folders)) < pageSize {
break
}
page++
}
return tuples, nil
@@ -3,6 +3,8 @@ package annotationsimpl
import (
"context"
"github.com/prometheus/client_golang/prometheus"
"github.com/grafana/grafana/pkg/services/annotations/accesscontrol"
"github.com/grafana/grafana/pkg/services/annotations/annotationsimpl/loki"
"github.com/grafana/grafana/pkg/services/dashboards"
@@ -33,6 +35,7 @@ func ProvideService(
tracer tracing.Tracer,
ruleStore *alertingStore.DBstore,
dashSvc dashboards.DashboardService,
reg prometheus.Registerer,
) *RepositoryImpl {
l := log.New("annotations")
l.Debug("Initializing annotations service")
@@ -41,7 +44,7 @@ func ProvideService(
write := xormStore
var read readStore
historianStore := loki.NewLokiHistorianStore(cfg.UnifiedAlerting.StateHistory, db, ruleStore, log.New("annotations.loki"), tracer)
historianStore := loki.NewLokiHistorianStore(cfg.UnifiedAlerting.StateHistory, db, ruleStore, log.New("annotations.loki"), tracer, reg)
if historianStore != nil {
l.Debug("Using composite read store")
read = NewCompositeStore(log.New("annotations.composite"), xormStore, historianStore)
@@ -6,6 +6,7 @@ import (
"fmt"
"testing"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -70,7 +71,7 @@ func TestIntegrationAnnotationListingWithRBAC(t *testing.T) {
kvstore.NewFakeKVStore())
require.NoError(t, err)
dashSvc.RegisterDashboardPermissions(accesscontrolmock.NewMockedPermissionsService())
repo := ProvideService(sql, cfg, features, tagService, tracing.InitializeTracerForTest(), ruleStore, dashSvc)
repo := ProvideService(sql, cfg, features, tagService, tracing.InitializeTracerForTest(), ruleStore, dashSvc, prometheus.NewPedanticRegistry())
dashboard1 := testutil.CreateDashboard(t, sql, cfg, features, dashboards.SaveDashboardCommand{
UserID: 1,
@@ -344,7 +345,7 @@ func TestIntegrationAnnotationListingWithInheritedRBAC(t *testing.T) {
cfg := setting.NewCfg()
cfg.AnnotationMaximumTagsLength = 60
ruleStore := alertingStore.SetupStoreForTesting(t, sql)
repo := ProvideService(sql, cfg, tc.features, tagimpl.ProvideService(sql), tracing.InitializeTracerForTest(), ruleStore, dashSvc)
repo := ProvideService(sql, cfg, tc.features, tagimpl.ProvideService(sql), tracing.InitializeTracerForTest(), ruleStore, dashSvc, prometheus.NewPedanticRegistry())
usr.Permissions = map[int64]map[string][]string{1: tc.permissions}
testutil.SetupRBACPermission(t, sql, role, usr)
@@ -56,7 +56,7 @@ type LokiHistorianStore struct {
ruleStore RuleStore
}
func NewLokiHistorianStore(cfg setting.UnifiedAlertingStateHistorySettings, db db.DB, ruleStore RuleStore, log log.Logger, tracer tracing.Tracer) *LokiHistorianStore {
func NewLokiHistorianStore(cfg setting.UnifiedAlertingStateHistorySettings, db db.DB, ruleStore RuleStore, log log.Logger, tracer tracing.Tracer, reg prometheus.Registerer) *LokiHistorianStore {
if !useStore(cfg) {
return nil
}
@@ -67,7 +67,7 @@ func NewLokiHistorianStore(cfg setting.UnifiedAlertingStateHistorySettings, db d
}
return &LokiHistorianStore{
client: historian.NewLokiClient(lokiCfg, historian.NewRequester(), ngmetrics.NewHistorianMetrics(prometheus.DefaultRegisterer, subsystem), log, tracer),
client: historian.NewLokiClient(lokiCfg, historian.NewRequester(), ngmetrics.NewHistorianMetrics(reg, subsystem), log, tracer),
db: db,
log: log,
ruleStore: ruleStore,

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