merge main
This commit is contained in:
+7
-10
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]{
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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"))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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,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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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=
|
||||
|
||||
@@ -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{}
|
||||
}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
+17
-8
@@ -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
|
||||
|
||||
Vendored
+18
-9
@@ -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
|
||||
|
||||
Vendored
+16
-7
@@ -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
|
||||
|
||||
+17
-8
@@ -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
|
||||
|
||||
+18
-9
@@ -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
|
||||
|
||||
Vendored
+16
-7
@@ -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
|
||||
|
||||
Vendored
+18
-9
@@ -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
|
||||
|
||||
+17
-8
@@ -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
|
||||
|
||||
Vendored
+18
-9
@@ -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
|
||||
|
||||
Vendored
+16
-7
@@ -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
|
||||
|
||||
+17
-8
@@ -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
|
||||
|
||||
+18
-9
@@ -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
|
||||
|
||||
Vendored
+16
-7
@@ -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
|
||||
|
||||
Vendored
+18
-9
@@ -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
|
||||
|
||||
+17
-8
@@ -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
|
||||
|
||||
Vendored
+18
-9
@@ -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
|
||||
|
||||
Vendored
+16
-7
@@ -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
|
||||
|
||||
+17
-8
@@ -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
|
||||
|
||||
+18
-9
@@ -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
|
||||
|
||||
Vendored
+16
-7
@@ -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
|
||||
|
||||
Vendored
+18
-9
@@ -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) {
|
||||
|
||||
@@ -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{}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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())
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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{}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user