Fix listing and getting dashboard versions across different API versions (#109860)
* Fix listing and getting dashboard versions across different API versions What This commit updates dashboard version service to use API version aware API client. The service now also supports parsing different API version representation of dashboards. The API version aware client is also updated to support listing across versions. Why Currently listing or getting specific versions is broken for all v2 versions of the dashboard API, especially if the dashboard being checked is still saved using v1 APIs. Signed-off-by: Igor Suleymanov <igor.suleymanov@grafana.com> * Remove superfluous tracing spans Signed-off-by: Igor Suleymanov <igor.suleymanov@grafana.com> --------- Signed-off-by: Igor Suleymanov <igor.suleymanov@grafana.com>
This commit is contained in:
@@ -232,7 +232,6 @@ require (
|
||||
|
||||
require (
|
||||
github.com/grafana/grafana/apps/advisor v0.0.0 // @grafana/plugins-platform-backend
|
||||
github.com/grafana/grafana/apps/alerting/alertenrichment v0.0.0 // @grafana/alerting-backend
|
||||
github.com/grafana/grafana/apps/alerting/notifications v0.0.0 // @grafana/alerting-backend
|
||||
github.com/grafana/grafana/apps/dashboard v0.0.0 // @grafana/grafana-app-platform-squad @grafana/dashboards-squad
|
||||
github.com/grafana/grafana/apps/folder v0.0.0 // @grafana/grafana-search-and-storage
|
||||
@@ -457,7 +456,6 @@ require (
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/jaegertracing/jaeger v1.67.0 // indirect
|
||||
github.com/jaegertracing/jaeger-idl v0.5.0 // indirect
|
||||
github.com/jcmturner/aescts/v2 v2.0.0 // indirect
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect
|
||||
@@ -519,9 +517,6 @@ require (
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/oklog/ulid/v2 v2.1.0 // indirect
|
||||
github.com/open-feature/go-sdk-contrib/providers/ofrep v0.1.5 // indirect
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.124.1 // indirect
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/core/xidutils v0.124.1 // indirect
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/jaeger v0.124.1 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/opentracing-contrib/go-stdlib v1.0.0 // indirect
|
||||
@@ -587,7 +582,6 @@ require (
|
||||
go.etcd.io/etcd/client/v3 v3.5.21 // indirect
|
||||
go.mongodb.org/mongo-driver v1.17.3 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/collector/semconv v0.124.0 // indirect
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.61.0 // indirect
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.36.0 // indirect
|
||||
go.opentelemetry.io/contrib/exporters/autoexport v0.61.0 // indirect
|
||||
|
||||
@@ -1793,8 +1793,6 @@ github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
|
||||
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jaegertracing/jaeger v1.67.0 h1:t0BiJZVW9D3Z16y3uHqKzV9bKFTusooTH1Kgr77xF2Q=
|
||||
github.com/jaegertracing/jaeger v1.67.0/go.mod h1:tE/FEQfybCSdUbBgel51YaCSkc58O+Njih8oTl6j8vw=
|
||||
github.com/jaegertracing/jaeger-idl v0.5.0 h1:zFXR5NL3Utu7MhPg8ZorxtCBjHrL3ReM1VoB65FOFGE=
|
||||
github.com/jaegertracing/jaeger-idl v0.5.0/go.mod h1:ON90zFo9eoyXrt9F/KN8YeF3zxcnujaisMweFY/rg5k=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
|
||||
@@ -2107,12 +2105,6 @@ github.com/open-feature/go-sdk-contrib/providers/go-feature-flag v0.2.3 h1:6jpO6
|
||||
github.com/open-feature/go-sdk-contrib/providers/go-feature-flag v0.2.3/go.mod h1:dPUHjAIFzg+ci/wt6XxlNiiMkOh5Yw4SGyeRY0AFT0g=
|
||||
github.com/open-feature/go-sdk-contrib/providers/ofrep v0.1.5 h1:ZdqlGnNwhWf3luhBQlIpbglvcCzjkcuEgOEhYhr5Emc=
|
||||
github.com/open-feature/go-sdk-contrib/providers/ofrep v0.1.5/go.mod h1:jrD4UG3ZCzuwImKHlyuIN2iWeYjlOX5+zJ/sX45efuE=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.124.1 h1:NrjsoVPxI6lmV8jPImDcMeqYh+97Y71f/HB5Sfpfe3I=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.124.1/go.mod h1:AFMryJmht7dZxcAwc2sx/r4gxbriElWw49ugxKp2mcA=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/core/xidutils v0.124.1 h1:E1e96GTHmiAfIfeYfA5ZVnOxud3+vbisGp0gE1tfd4s=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/core/xidutils v0.124.1/go.mod h1:MOhFATtYSLad9nKunjh6uGf8nQUcWje2LPlhD2uu3do=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/jaeger v0.124.1 h1:9c6L4xlAMqhOg5y54Bc2B5t0i49yz7v2I6I8RY4Z0/o=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/jaeger v0.124.1/go.mod h1:6f0N58o0cOHC0ApSM/qrooVmQza1eQ7L53PDE91uO1Q=
|
||||
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
@@ -2552,13 +2544,8 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/collector v0.124.0 h1:g/dfdGFhBcQI0ggGxTmGlJnJ6Yl6T2gVxQoIj4UfXCc=
|
||||
go.opentelemetry.io/collector/pdata v1.30.0 h1:j3jyq9um436r6WzWySzexP2nLnFdmL5uVBYAlyr9nDM=
|
||||
go.opentelemetry.io/collector/pdata v1.30.0/go.mod h1:0Bxu1ktuj4wE7PIASNSvd0SdBscQ1PLtYasymJ13/Cs=
|
||||
go.opentelemetry.io/collector/pdata/pprofile v0.124.0 h1:ZjL9wKqzP4BHj0/F1jfGxs1Va8B7xmYayipZeNVoWJE=
|
||||
go.opentelemetry.io/collector/pdata/pprofile v0.124.0/go.mod h1:1EN3Gw5LSI4fSVma/Yfv/6nqeuYgRTm1/kmG5nE5Oyo=
|
||||
go.opentelemetry.io/collector/semconv v0.124.0 h1:YTdo3UFwNyDQCh9DiSm2rbzAgBuwn/9dNZ0rv454goA=
|
||||
go.opentelemetry.io/collector/semconv v0.124.0/go.mod h1:te6VQ4zZJO5Lp8dM2XIhDxDiL45mwX0YAQQWRQ0Qr9U=
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.61.0 h1:RyrtJzu5MAmIcbRrwg75b+w3RlZCP0vJByDVzcpAe3M=
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.61.0/go.mod h1:tirr4p9NXbzjlbruiRGp53IzlYrDk5CO2fdHj0sSSaY=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.36.0 h1:F7q2tNlCaHY9nMKHR6XH9/qkp8FktLnIcy6jJNyOCQw=
|
||||
|
||||
@@ -37,6 +37,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards/database"
|
||||
dashboardservice "github.com/grafana/grafana/pkg/services/dashboards/service"
|
||||
dashclient "github.com/grafana/grafana/pkg/services/dashboards/service/client"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/folder/folderimpl"
|
||||
"github.com/grafana/grafana/pkg/services/licensing/licensingtest"
|
||||
@@ -446,11 +447,32 @@ func setupServer(b testing.TB, sc benchScenario, features featuremgmt.FeatureTog
|
||||
cfg, features, routing.NewRouteRegister(), sc.db, ac, license, folderServiceWithFlagOn, acSvc, sc.teamSvc, sc.userSvc, actionSets)
|
||||
require.NoError(b, err)
|
||||
dashboardSvc, err := dashboardservice.ProvideDashboardServiceImpl(
|
||||
sc.cfg, dashStore, folderStore,
|
||||
features, folderPermissions, ac, actest.FakeService{},
|
||||
folderServiceWithFlagOn, nil, client.MockTestRestConfig{}, nil, quotaSrv, nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService(),
|
||||
sc.cfg,
|
||||
dashStore,
|
||||
folderStore,
|
||||
features,
|
||||
folderPermissions,
|
||||
ac,
|
||||
actest.FakeService{},
|
||||
folderServiceWithFlagOn,
|
||||
nil,
|
||||
quotaSrv,
|
||||
nil,
|
||||
nil,
|
||||
dualwrite.ProvideTestService(),
|
||||
serverlock.ProvideService(sc.db, tracing.InitializeTracerForTest()),
|
||||
kvstore.NewFakeKVStore(),
|
||||
dashclient.NewK8sClientWithFallback(
|
||||
sc.cfg,
|
||||
client.MockTestRestConfig{},
|
||||
dashStore,
|
||||
sc.userSvc,
|
||||
nil,
|
||||
sort.ProvideService(),
|
||||
dualwrite.ProvideTestService(),
|
||||
nil,
|
||||
features,
|
||||
),
|
||||
)
|
||||
require.NoError(b, err)
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ import (
|
||||
_ "github.com/robfig/cron/v3"
|
||||
_ "github.com/russellhaering/goxmldsig"
|
||||
_ "github.com/spf13/cobra" // used by the standalone apiserver cli
|
||||
_ "github.com/spyzhov/ajson"
|
||||
_ "github.com/stretchr/testify/require"
|
||||
_ "gocloud.dev/secrets/awskms"
|
||||
_ "gocloud.dev/secrets/azurekeyvault"
|
||||
@@ -53,6 +52,5 @@ import (
|
||||
_ "github.com/grafana/e2e"
|
||||
_ "github.com/grafana/gofpdf"
|
||||
_ "github.com/grafana/gomemcache/memcache"
|
||||
_ "github.com/grafana/grafana/apps/alerting/alertenrichment/pkg/apis/alertenrichment/v0alpha1"
|
||||
_ "github.com/grafana/tempo/pkg/traceql"
|
||||
_ "github.com/spyzhov/ajson"
|
||||
)
|
||||
|
||||
@@ -79,6 +79,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
dashboardstore "github.com/grafana/grafana/pkg/services/dashboards/database"
|
||||
dashboardservice "github.com/grafana/grafana/pkg/services/dashboards/service"
|
||||
dashboardclient "github.com/grafana/grafana/pkg/services/dashboards/service/client"
|
||||
"github.com/grafana/grafana/pkg/services/dashboardsnapshots"
|
||||
dashsnapstore "github.com/grafana/grafana/pkg/services/dashboardsnapshots/database"
|
||||
dashsnapsvc "github.com/grafana/grafana/pkg/services/dashboardsnapshots/service"
|
||||
@@ -457,6 +458,8 @@ var wireBasicSet = wire.NewSet(
|
||||
grafanaapiserver.WireSet,
|
||||
apiregistry.WireSet,
|
||||
appregistry.WireSet,
|
||||
// Dashboard Kubernetes helpers
|
||||
dashboardclient.ProvideK8sClientWithFallback,
|
||||
)
|
||||
|
||||
var wireSet = wire.NewSet(
|
||||
|
||||
+14
-11
File diff suppressed because one or more lines are too long
@@ -6,6 +6,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"golang.org/x/sync/errgroup"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
@@ -26,8 +27,22 @@ import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const (
|
||||
// maxConcurrentGetRequests is the maximum number of concurrent Get requests to the K8s API.
|
||||
// It is used to determine max amount of concurrent version fallback requests when handling a List request.
|
||||
// TODO: do we need to make this configurable via config.ini?
|
||||
maxConcurrentGetRequests = 10
|
||||
)
|
||||
|
||||
// K8sHandlerWithFallback is a wrapper around the K8sHandler that provides a fallback to the stored version.
|
||||
type K8sHandlerWithFallback interface {
|
||||
client.K8sHandler
|
||||
}
|
||||
|
||||
// K8sClientFactory creates a K8sHandler for a given version.
|
||||
type K8sClientFactory func(ctx context.Context, version string) client.K8sHandler
|
||||
|
||||
// K8sClientWithFallback is a wrapper around the K8sHandler that provides a fallback to the stored version.
|
||||
type K8sClientWithFallback struct {
|
||||
client.K8sHandler
|
||||
|
||||
@@ -36,6 +51,24 @@ type K8sClientWithFallback struct {
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
// ProvideK8sClientWithFallback provides a K8sHandlerWithFallback.
|
||||
func ProvideK8sClientWithFallback(
|
||||
cfg *setting.Cfg,
|
||||
restConfigProvider apiserver.RestConfigProvider,
|
||||
dashboardStore dashboards.Store,
|
||||
userService user.Service,
|
||||
resourceClient resource.ResourceClient,
|
||||
featureToggles featuremgmt.FeatureToggles,
|
||||
dualWriter dualwrite.Service,
|
||||
sorter sort.Service,
|
||||
reg prometheus.Registerer,
|
||||
) K8sHandlerWithFallback {
|
||||
return NewK8sClientWithFallback(
|
||||
cfg, restConfigProvider, dashboardStore, userService, resourceClient, sorter, dualWriter, reg, featureToggles,
|
||||
)
|
||||
}
|
||||
|
||||
// NewK8sClientWithFallback creates a new K8sClientWithFallback.
|
||||
func NewK8sClientWithFallback(
|
||||
cfg *setting.Cfg,
|
||||
restConfigProvider apiserver.RestConfigProvider,
|
||||
@@ -56,8 +89,12 @@ func NewK8sClientWithFallback(
|
||||
}
|
||||
}
|
||||
|
||||
func (h *K8sClientWithFallback) Get(ctx context.Context, name string, orgID int64, options metav1.GetOptions, subresources ...string) (*unstructured.Unstructured, error) {
|
||||
spanCtx, span := tracing.Start(ctx, "versionFallbackK8sHandler.Get")
|
||||
// Get gets a resource from the K8s API.
|
||||
// If the resource indicates that it was stored using a different version, the client will re-fetch it using the stored version.
|
||||
func (h *K8sClientWithFallback) Get(
|
||||
ctx context.Context, name string, orgID int64, options metav1.GetOptions, subresources ...string,
|
||||
) (*unstructured.Unstructured, error) {
|
||||
spanCtx, span := tracing.Start(ctx, "K8sClientWithFallback.Get")
|
||||
defer span.End()
|
||||
|
||||
span.SetAttributes(
|
||||
@@ -91,6 +128,135 @@ func (h *K8sClientWithFallback) Get(ctx context.Context, name string, orgID int6
|
||||
return h.newClientFunc(spanCtx, storedVersion).Get(spanCtx, name, orgID, options, subresources...)
|
||||
}
|
||||
|
||||
type nameAndResourceVersion struct {
|
||||
Name string
|
||||
ResourceVersion string
|
||||
}
|
||||
|
||||
// List lists resources from the K8s API.
|
||||
// It will check individual resources and re-fetch them if necessary.
|
||||
func (h *K8sClientWithFallback) List(
|
||||
ctx context.Context, orgID int64, options metav1.ListOptions,
|
||||
) (*unstructured.UnstructuredList, error) {
|
||||
ctx, span := tracing.Start(ctx, "K8sClientWithFallback.List")
|
||||
defer span.End()
|
||||
|
||||
span.SetAttributes(
|
||||
attribute.Int64("org.id", orgID),
|
||||
attribute.Bool("fallback", false),
|
||||
)
|
||||
|
||||
initial, err := h.K8sHandler.List(ctx, orgID, options)
|
||||
if err != nil {
|
||||
h.log.Error("failed to fetch initial list", "error", err)
|
||||
return nil, tracing.Error(span, err)
|
||||
}
|
||||
|
||||
res := initial.DeepCopy()
|
||||
res.Items = res.Items[:0]
|
||||
|
||||
// Map of version -> list of names to fetch.
|
||||
toFetch := make(map[string][]nameAndResourceVersion)
|
||||
|
||||
for _, item := range initial.Items {
|
||||
failed, storedVersion, conversionErr := getConversionStatus(&item)
|
||||
if !failed {
|
||||
res.Items = append(res.Items, item)
|
||||
continue
|
||||
}
|
||||
|
||||
h.log.Debug(
|
||||
"will fetch object with the stored version",
|
||||
"name", item.GetName(),
|
||||
"storedVersion", storedVersion,
|
||||
"conversionErr", conversionErr,
|
||||
)
|
||||
h.metrics.fallbackCounter.WithLabelValues(storedVersion).Inc()
|
||||
|
||||
names, ok := toFetch[storedVersion]
|
||||
if !ok {
|
||||
names = make([]nameAndResourceVersion, 0)
|
||||
}
|
||||
|
||||
names = append(names, nameAndResourceVersion{
|
||||
Name: item.GetName(),
|
||||
ResourceVersion: item.GetResourceVersion(),
|
||||
})
|
||||
toFetch[storedVersion] = names
|
||||
}
|
||||
|
||||
for version, names := range toFetch {
|
||||
h.log.Info(
|
||||
"fetching multiple objects with the stored version",
|
||||
"version", version,
|
||||
)
|
||||
|
||||
// Log names at debug level, because there could be a lot of them.
|
||||
h.log.Debug(
|
||||
"will fetch objects with names",
|
||||
"names", names,
|
||||
)
|
||||
|
||||
items, err := h.fetchWithVersion(ctx, orgID, version, names...)
|
||||
if err != nil {
|
||||
return nil, tracing.Error(span, err)
|
||||
}
|
||||
|
||||
res.Items = append(res.Items, items...)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// fetchWithVersion fetches multiple resources from the K8s API.
|
||||
// It uses concurrent Get requests, one for each name.
|
||||
//
|
||||
// TODO: maybe consider using List with a field selector and / or search?
|
||||
//
|
||||
// items, err := client.List(spanCtx, orgID, metav1.ListOptions{
|
||||
// FieldSelector: fmt.Sprintf("metadata.name in (%s)", strings.Join(names, ",")),
|
||||
// })
|
||||
func (h *K8sClientWithFallback) fetchWithVersion(
|
||||
ctx context.Context, orgID int64, version string, items ...nameAndResourceVersion,
|
||||
) ([]unstructured.Unstructured, error) {
|
||||
ctx, span := tracing.Start(ctx, "K8sClientWithFallback.fetchWithVersion")
|
||||
defer span.End()
|
||||
|
||||
span.SetAttributes(
|
||||
attribute.String("version", version),
|
||||
attribute.Int("count", len(items)),
|
||||
)
|
||||
|
||||
client := h.newClientFunc(ctx, version)
|
||||
|
||||
g, ctx := errgroup.WithContext(ctx)
|
||||
g.SetLimit(maxConcurrentGetRequests)
|
||||
res := make([]unstructured.Unstructured, len(items))
|
||||
|
||||
for i, it := range items {
|
||||
g.Go(func() error {
|
||||
item, err := client.Get(ctx, it.Name, orgID, metav1.GetOptions{
|
||||
ResourceVersion: it.ResourceVersion,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// NB: it's important to set via the index,
|
||||
// because `append`ing would create a race condition.
|
||||
res[i] = *item
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, tracing.Error(span, err)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func getConversionStatus(obj *unstructured.Unstructured) (failed bool, storedVersion string, conversionErr string) {
|
||||
status, found, _ := unstructured.NestedMap(obj.Object, "status")
|
||||
if !found {
|
||||
|
||||
@@ -195,6 +195,196 @@ func TestK8sHandlerWithFallback_Get(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestK8sHandlerWithFallback_List(t *testing.T) {
|
||||
// Helper function to create a dashboard item
|
||||
createDashboard := func(name, resourceVersion string, status map[string]interface{}) unstructured.Unstructured {
|
||||
return unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"name": name,
|
||||
"resourceVersion": resourceVersion,
|
||||
},
|
||||
"status": status,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to create a fallback dashboard item
|
||||
createFallbackDashboard := func(name, resourceVersion, apiVersion string) unstructured.Unstructured {
|
||||
return unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": apiVersion,
|
||||
"kind": "Dashboard",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": name,
|
||||
"resourceVersion": resourceVersion,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to create conversion status
|
||||
conversionStatus := func(failed bool, storedVersion, errorMsg string) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"conversion": map[string]interface{}{
|
||||
"failed": failed,
|
||||
"storedVersion": storedVersion,
|
||||
"error": errorMsg,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("List without fallback needed", func(t *testing.T) {
|
||||
setup := setupTest(t)
|
||||
expectedResult := &unstructured.UnstructuredList{
|
||||
Items: []unstructured.Unstructured{
|
||||
createDashboard("dashboard-1", "123", map[string]interface{}{"someOtherStatus": "ok"}),
|
||||
createDashboard("dashboard-2", "456", map[string]interface{}{"anotherStatus": "ok"}),
|
||||
},
|
||||
}
|
||||
|
||||
setup.mockClientV1Alpha1.On("List", mock.Anything, int64(1), metav1.ListOptions{}).Return(expectedResult, nil).Once()
|
||||
|
||||
result, err := setup.handler.List(context.Background(), 1, metav1.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedResult, result)
|
||||
|
||||
setup.mockClientV1Alpha1.AssertExpectations(t)
|
||||
setup.mockClientV2Alpha1.AssertExpectations(t)
|
||||
})
|
||||
|
||||
t.Run("List with some items needing fallback", func(t *testing.T) {
|
||||
setup := setupTest(t)
|
||||
initialResult := &unstructured.UnstructuredList{
|
||||
Items: []unstructured.Unstructured{
|
||||
createDashboard("dashboard-ok", "123", map[string]interface{}{"someOtherStatus": "ok"}),
|
||||
createDashboard("dashboard-fallback", "456", conversionStatus(true, "v2alpha1", "conversion failed")),
|
||||
},
|
||||
}
|
||||
|
||||
fallbackResult := createFallbackDashboard("dashboard-fallback", "456", "dashboard/v2alpha1")
|
||||
|
||||
setup.mockClientV1Alpha1.On("List", mock.Anything, int64(2), metav1.ListOptions{}).Return(initialResult, nil).Once()
|
||||
setup.mockClientV2Alpha1.On("Get", mock.Anything, "dashboard-fallback", int64(2), metav1.GetOptions{ResourceVersion: "456"}, mock.Anything).Return(&fallbackResult, nil).Once()
|
||||
|
||||
result, err := setup.handler.List(context.Background(), 2, metav1.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, result.Items, 2)
|
||||
|
||||
expectedItems := []unstructured.Unstructured{
|
||||
createDashboard("dashboard-ok", "123", map[string]interface{}{"someOtherStatus": "ok"}),
|
||||
fallbackResult,
|
||||
}
|
||||
require.ElementsMatch(t, expectedItems, result.Items)
|
||||
|
||||
setup.mockClientV1Alpha1.AssertExpectations(t)
|
||||
setup.mockClientV2Alpha1.AssertExpectations(t)
|
||||
})
|
||||
|
||||
t.Run("List with all items needing fallback", func(t *testing.T) {
|
||||
setup := setupTest(t)
|
||||
initialResult := &unstructured.UnstructuredList{
|
||||
Items: []unstructured.Unstructured{
|
||||
createDashboard("dashboard-1-fallback", "111", conversionStatus(true, "v2alpha1", "conversion failed 1")),
|
||||
createDashboard("dashboard-2-fallback", "222", conversionStatus(true, "v2alpha1", "conversion failed 2")),
|
||||
},
|
||||
}
|
||||
|
||||
fallbackResult1 := createFallbackDashboard("dashboard-1-fallback", "111", "dashboard/v2alpha1")
|
||||
fallbackResult2 := createFallbackDashboard("dashboard-2-fallback", "222", "dashboard/v2alpha1")
|
||||
|
||||
setup.mockClientV1Alpha1.On("List", mock.Anything, int64(3), metav1.ListOptions{}).Return(initialResult, nil).Once()
|
||||
setup.mockClientV2Alpha1.On("Get", mock.Anything, "dashboard-1-fallback", int64(3), metav1.GetOptions{ResourceVersion: "111"}, mock.Anything).Return(&fallbackResult1, nil).Once()
|
||||
setup.mockClientV2Alpha1.On("Get", mock.Anything, "dashboard-2-fallback", int64(3), metav1.GetOptions{ResourceVersion: "222"}, mock.Anything).Return(&fallbackResult2, nil).Once()
|
||||
|
||||
result, err := setup.handler.List(context.Background(), 3, metav1.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, result.Items, 2)
|
||||
|
||||
expectedItems := []unstructured.Unstructured{fallbackResult1, fallbackResult2}
|
||||
require.ElementsMatch(t, expectedItems, result.Items)
|
||||
|
||||
setup.mockClientV1Alpha1.AssertExpectations(t)
|
||||
setup.mockClientV2Alpha1.AssertExpectations(t)
|
||||
})
|
||||
|
||||
t.Run("List with different versions needing fallback", func(t *testing.T) {
|
||||
setup := setupTest(t)
|
||||
initialResult := &unstructured.UnstructuredList{
|
||||
Items: []unstructured.Unstructured{
|
||||
createDashboard("dashboard-v2alpha1", "333", conversionStatus(true, "v2alpha1", "conversion failed v2alpha1")),
|
||||
createDashboard("dashboard-v1beta1", "444", conversionStatus(true, "v1beta1", "conversion failed v1beta1")),
|
||||
},
|
||||
}
|
||||
|
||||
fallbackResultV2Alpha1 := createFallbackDashboard("dashboard-v2alpha1", "333", "dashboard/v2alpha1")
|
||||
fallbackResultV1Beta1 := createFallbackDashboard("dashboard-v1beta1", "444", "dashboard/v1beta1")
|
||||
|
||||
setup.mockClientV1Alpha1.On("List", mock.Anything, int64(4), metav1.ListOptions{}).Return(initialResult, nil).Once()
|
||||
setup.mockClientV2Alpha1.On("Get", mock.Anything, "dashboard-v2alpha1", int64(4), metav1.GetOptions{ResourceVersion: "333"}, mock.Anything).Return(&fallbackResultV2Alpha1, nil).Once()
|
||||
setup.mockClientV1Alpha1.On("Get", mock.Anything, "dashboard-v1beta1", int64(4), metav1.GetOptions{ResourceVersion: "444"}, mock.Anything).Return(&fallbackResultV1Beta1, nil).Once()
|
||||
|
||||
result, err := setup.handler.List(context.Background(), 4, metav1.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, result.Items, 2)
|
||||
|
||||
expectedItems := []unstructured.Unstructured{fallbackResultV2Alpha1, fallbackResultV1Beta1}
|
||||
require.ElementsMatch(t, expectedItems, result.Items)
|
||||
|
||||
setup.mockClientV1Alpha1.AssertExpectations(t)
|
||||
setup.mockClientV2Alpha1.AssertExpectations(t)
|
||||
})
|
||||
|
||||
t.Run("List with initial fetch error", func(t *testing.T) {
|
||||
setup := setupTest(t)
|
||||
expectedErr := errors.New("initial list failed")
|
||||
|
||||
setup.mockClientV1Alpha1.On("List", mock.Anything, int64(5), metav1.ListOptions{}).Return(nil, expectedErr).Once()
|
||||
|
||||
_, err := setup.handler.List(context.Background(), 5, metav1.ListOptions{})
|
||||
require.Error(t, err)
|
||||
require.Equal(t, expectedErr, err)
|
||||
|
||||
setup.mockClientV1Alpha1.AssertExpectations(t)
|
||||
setup.mockClientV2Alpha1.AssertExpectations(t)
|
||||
})
|
||||
|
||||
t.Run("List with fallback fetch error", func(t *testing.T) {
|
||||
setup := setupTest(t)
|
||||
initialResult := &unstructured.UnstructuredList{
|
||||
Items: []unstructured.Unstructured{
|
||||
createDashboard("dashboard-fallback-error", "555", conversionStatus(true, "v2alpha1", "conversion failed")),
|
||||
},
|
||||
}
|
||||
|
||||
fallbackErr := errors.New("fallback get failed")
|
||||
|
||||
setup.mockClientV1Alpha1.On("List", mock.Anything, int64(6), metav1.ListOptions{}).Return(initialResult, nil).Once()
|
||||
setup.mockClientV2Alpha1.On("Get", mock.Anything, "dashboard-fallback-error", int64(6), metav1.GetOptions{ResourceVersion: "555"}, mock.Anything).Return(nil, fallbackErr).Once()
|
||||
|
||||
_, err := setup.handler.List(context.Background(), 6, metav1.ListOptions{})
|
||||
require.Error(t, err)
|
||||
require.Equal(t, fallbackErr, err)
|
||||
|
||||
setup.mockClientV1Alpha1.AssertExpectations(t)
|
||||
setup.mockClientV2Alpha1.AssertExpectations(t)
|
||||
})
|
||||
|
||||
t.Run("List with empty result", func(t *testing.T) {
|
||||
setup := setupTest(t)
|
||||
emptyResult := &unstructured.UnstructuredList{Items: []unstructured.Unstructured{}}
|
||||
|
||||
setup.mockClientV1Alpha1.On("List", mock.Anything, int64(7), metav1.ListOptions{}).Return(emptyResult, nil).Once()
|
||||
|
||||
result, err := setup.handler.List(context.Background(), 7, metav1.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, result.Items, 0)
|
||||
|
||||
setup.mockClientV1Alpha1.AssertExpectations(t)
|
||||
setup.mockClientV2Alpha1.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetConversionStatus(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -39,8 +39,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/dashboard/legacysearcher"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/client"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
|
||||
@@ -52,7 +50,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/publicdashboards"
|
||||
"github.com/grafana/grafana/pkg/services/quota"
|
||||
"github.com/grafana/grafana/pkg/services/search/model"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
|
||||
"github.com/grafana/grafana/pkg/services/store/entity"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
@@ -94,7 +91,7 @@ type DashboardServiceImpl struct {
|
||||
dashboardPermissions accesscontrol.DashboardPermissionsService
|
||||
ac accesscontrol.AccessControl
|
||||
acService accesscontrol.Service
|
||||
k8sclient client.K8sHandler
|
||||
k8sclient dashboardclient.K8sHandlerWithFallback
|
||||
metrics *dashboardsMetrics
|
||||
publicDashboardService publicdashboards.ServiceWrapper
|
||||
serverLockService *serverlock.ServerLockService
|
||||
@@ -374,16 +371,23 @@ var _ registry.BackgroundService = (*DashboardServiceImpl)(nil)
|
||||
|
||||
// This is the uber service that implements a three smaller services
|
||||
func ProvideDashboardServiceImpl(
|
||||
cfg *setting.Cfg, dashboardStore dashboards.Store, folderStore folder.FolderStore,
|
||||
features featuremgmt.FeatureToggles, folderPermissionsService accesscontrol.FolderPermissionsService,
|
||||
ac accesscontrol.AccessControl, acService accesscontrol.Service, folderSvc folder.Service, r prometheus.Registerer,
|
||||
restConfigProvider apiserver.RestConfigProvider, userService user.Service,
|
||||
quotaService quota.Service, orgService org.Service, publicDashboardService publicdashboards.ServiceWrapper,
|
||||
resourceClient resource.ResourceClient, dual dualwrite.Service, sorter sort.Service,
|
||||
cfg *setting.Cfg,
|
||||
dashboardStore dashboards.Store,
|
||||
folderStore folder.FolderStore,
|
||||
features featuremgmt.FeatureToggles,
|
||||
folderPermissionsService accesscontrol.FolderPermissionsService,
|
||||
ac accesscontrol.AccessControl,
|
||||
acService accesscontrol.Service,
|
||||
folderSvc folder.Service,
|
||||
r prometheus.Registerer,
|
||||
quotaService quota.Service,
|
||||
orgService org.Service,
|
||||
publicDashboardService publicdashboards.ServiceWrapper,
|
||||
dual dualwrite.Service,
|
||||
serverLockService *serverlock.ServerLockService,
|
||||
kvstore kvstore.KVStore,
|
||||
k8sClient dashboardclient.K8sHandlerWithFallback,
|
||||
) (*DashboardServiceImpl, error) {
|
||||
k8sclient := dashboardclient.NewK8sClientWithFallback(cfg, restConfigProvider, dashboardStore, userService, resourceClient, sorter, dual, r, features)
|
||||
dashSvc := &DashboardServiceImpl{
|
||||
cfg: cfg,
|
||||
log: log.New("dashboard-service"),
|
||||
@@ -395,7 +399,7 @@ func ProvideDashboardServiceImpl(
|
||||
folderStore: folderStore,
|
||||
folderService: folderSvc,
|
||||
orgService: orgService,
|
||||
k8sclient: k8sclient,
|
||||
k8sclient: k8sClient,
|
||||
metrics: newDashboardsMetrics(r),
|
||||
dashboardPermissionsReady: make(chan struct{}),
|
||||
publicDashboardService: publicDashboardService,
|
||||
|
||||
@@ -11,22 +11,18 @@ import (
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
|
||||
dashboardv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1"
|
||||
dashboardv2beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/client"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
dashboardclient "github.com/grafana/grafana/pkg/services/dashboards/service/client"
|
||||
dashver "github.com/grafana/grafana/pkg/services/dashboardversion"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -38,33 +34,28 @@ type Service struct {
|
||||
cfg *setting.Cfg
|
||||
store store
|
||||
dashSvc dashboards.DashboardService
|
||||
k8sclient client.K8sHandler
|
||||
k8sclient dashboardclient.K8sHandlerWithFallback
|
||||
features featuremgmt.FeatureToggles
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func ProvideService(cfg *setting.Cfg, db db.DB, dashboardService dashboards.DashboardService, dashboardStore dashboards.Store, features featuremgmt.FeatureToggles,
|
||||
restConfigProvider apiserver.RestConfigProvider, userService user.Service, unified resource.ResourceClient, dual dualwrite.Service, sorter sort.Service) dashver.Service {
|
||||
func ProvideService(
|
||||
cfg *setting.Cfg,
|
||||
db db.DB,
|
||||
dashboardService dashboards.DashboardService,
|
||||
features featuremgmt.FeatureToggles,
|
||||
clientWithFallback dashboardclient.K8sHandlerWithFallback,
|
||||
) dashver.Service {
|
||||
return &Service{
|
||||
cfg: cfg,
|
||||
store: &sqlStore{
|
||||
db: db,
|
||||
dialect: db.GetDialect(),
|
||||
},
|
||||
features: features,
|
||||
k8sclient: client.NewK8sHandler(
|
||||
dual,
|
||||
request.GetNamespaceMapper(cfg),
|
||||
dashv0.DashboardResourceInfo.GroupVersionResource(),
|
||||
restConfigProvider.GetRestConfig,
|
||||
dashboardStore,
|
||||
userService,
|
||||
unified,
|
||||
sorter,
|
||||
features,
|
||||
),
|
||||
dashSvc: dashboardService,
|
||||
log: log.New("dashboard-version"),
|
||||
features: features,
|
||||
k8sclient: clientWithFallback,
|
||||
dashSvc: dashboardService,
|
||||
log: log.New("dashboard-version"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,25 +283,12 @@ func (s *Service) UnstructuredToLegacyDashboardVersionList(ctx context.Context,
|
||||
}
|
||||
|
||||
func (s *Service) unstructuredToLegacyDashboardVersionWithUsers(item *unstructured.Unstructured, users map[string]*user.User) (*dashver.DashboardVersionDTO, error) {
|
||||
spec, ok := item.Object["spec"].(map[string]any)
|
||||
if !ok {
|
||||
return nil, errors.New("error parsing dashboard from k8s response")
|
||||
}
|
||||
obj, err := utils.MetaAccessor(item)
|
||||
if err != nil {
|
||||
var vspec DashboardVersionSpec
|
||||
if err := UnstructuredToDashboardVersionSpec(item, &vspec); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uid := obj.GetName()
|
||||
spec["uid"] = uid
|
||||
|
||||
dashVersion := obj.GetGeneration()
|
||||
parentVersion := dashVersion - 1
|
||||
if parentVersion < 0 {
|
||||
parentVersion = 0
|
||||
}
|
||||
if dashVersion > 0 {
|
||||
spec["version"] = dashVersion
|
||||
}
|
||||
obj := vspec.MetaAccessor
|
||||
|
||||
var createdBy *user.User
|
||||
if creator, ok := users[obj.GetCreatedBy()]; ok {
|
||||
@@ -338,16 +316,16 @@ func (s *Service) unstructuredToLegacyDashboardVersionWithUsers(item *unstructur
|
||||
}
|
||||
|
||||
return &dashver.DashboardVersionDTO{
|
||||
ID: dashVersion,
|
||||
ID: vspec.Version,
|
||||
DashboardID: obj.GetDeprecatedInternalID(), // nolint:staticcheck
|
||||
DashboardUID: uid,
|
||||
DashboardUID: vspec.UID,
|
||||
Created: created,
|
||||
CreatedBy: createdByID,
|
||||
Message: obj.GetMessage(),
|
||||
RestoredFrom: restoreVer,
|
||||
Version: int(dashVersion),
|
||||
ParentVersion: int(parentVersion),
|
||||
Data: simplejson.NewFromAny(spec),
|
||||
Version: int(vspec.Version),
|
||||
ParentVersion: int(vspec.ParentVersion),
|
||||
Data: simplejson.NewFromAny(vspec.Spec),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -369,3 +347,73 @@ func getRestoreVersion(msg string) (int, error) {
|
||||
}
|
||||
return ver, nil
|
||||
}
|
||||
|
||||
// DashboardVersionSpec contains the necessary fields to represent a dashboard version.
|
||||
type DashboardVersionSpec struct {
|
||||
UID string
|
||||
Version int64
|
||||
ParentVersion int64
|
||||
Spec any
|
||||
MetaAccessor utils.GrafanaMetaAccessor
|
||||
}
|
||||
|
||||
// UnstructuredToDashboardVersionSpec converts a k8s unstructured object to a DashboardVersionSpec.
|
||||
// It supports dashboard API versions v0alpha1 through v2beta1.
|
||||
func UnstructuredToDashboardVersionSpec(obj *unstructured.Unstructured, dst *DashboardVersionSpec) error {
|
||||
if obj.GetAPIVersion() == dashboardv2alpha1.GroupVersion.String() ||
|
||||
obj.GetAPIVersion() == dashboardv2beta1.GroupVersion.String() {
|
||||
spec, ok := obj.Object["spec"]
|
||||
if !ok {
|
||||
return errors.New("error parsing dashboard from k8s response")
|
||||
}
|
||||
|
||||
meta, err := utils.MetaAccessor(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
version := meta.GetGeneration()
|
||||
parentVersion := version - 1
|
||||
if parentVersion < 0 {
|
||||
parentVersion = 0
|
||||
}
|
||||
|
||||
dst.UID = obj.GetName()
|
||||
dst.Version = version
|
||||
dst.ParentVersion = parentVersion
|
||||
dst.Spec = spec
|
||||
dst.MetaAccessor = meta
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Otherwise we assume that we are dealing with a legacy dashboard API version (v0 / v1 / etc.)
|
||||
|
||||
spec, ok := obj.Object["spec"].(map[string]any)
|
||||
if !ok {
|
||||
return errors.New("error parsing dashboard from k8s response")
|
||||
}
|
||||
meta, err := utils.MetaAccessor(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uid := meta.GetName()
|
||||
spec["uid"] = uid
|
||||
|
||||
dashVersion := meta.GetGeneration()
|
||||
parentVersion := dashVersion - 1
|
||||
if parentVersion < 0 {
|
||||
parentVersion = 0
|
||||
}
|
||||
if dashVersion > 0 {
|
||||
spec["version"] = dashVersion
|
||||
}
|
||||
|
||||
dst.UID = uid
|
||||
dst.Version = dashVersion
|
||||
dst.ParentVersion = parentVersion
|
||||
dst.Spec = spec
|
||||
dst.MetaAccessor = meta
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -22,6 +22,9 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
|
||||
dashboardv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1"
|
||||
dashboardv2beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1"
|
||||
)
|
||||
|
||||
func TestDashboardVersionService(t *testing.T) {
|
||||
@@ -280,6 +283,234 @@ func TestListDashboardVersions(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestUnstructuredToDashboardVersionSpec(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
obj *unstructured.Unstructured
|
||||
expectedResult DashboardVersionSpec
|
||||
expectError bool
|
||||
errorMessage string
|
||||
checkSpec func(t *testing.T, spec any)
|
||||
}{
|
||||
{
|
||||
name: "should convert v2alpha1 dashboard correctly",
|
||||
obj: &unstructured.Unstructured{
|
||||
Object: map[string]any{
|
||||
"apiVersion": dashboardv2alpha1.GroupVersion.String(),
|
||||
"metadata": map[string]any{
|
||||
"name": "test-dashboard",
|
||||
"generation": int64(5),
|
||||
},
|
||||
"spec": map[string]any{
|
||||
"title": "Test Dashboard",
|
||||
"panels": []any{
|
||||
map[string]any{"id": 1, "title": "Panel 1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedResult: DashboardVersionSpec{
|
||||
UID: "test-dashboard",
|
||||
Version: 5,
|
||||
ParentVersion: 4,
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "should convert v2beta1 dashboard correctly",
|
||||
obj: &unstructured.Unstructured{
|
||||
Object: map[string]any{
|
||||
"apiVersion": dashboardv2beta1.GroupVersion.String(),
|
||||
"metadata": map[string]any{
|
||||
"name": "test-dashboard-v2",
|
||||
"generation": int64(10),
|
||||
},
|
||||
"spec": map[string]any{
|
||||
"title": "Test Dashboard V2",
|
||||
"tags": []string{"test", "dashboard"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedResult: DashboardVersionSpec{
|
||||
UID: "test-dashboard-v2",
|
||||
Version: 10,
|
||||
ParentVersion: 9,
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "should convert legacy dashboard API version correctly",
|
||||
obj: &unstructured.Unstructured{
|
||||
Object: map[string]any{
|
||||
"apiVersion": "dashboard.grafana.app/v1",
|
||||
"metadata": map[string]any{
|
||||
"name": "legacy-dashboard",
|
||||
"generation": int64(3),
|
||||
},
|
||||
"spec": map[string]any{
|
||||
"title": "Legacy Dashboard",
|
||||
"uid": "legacy-uid",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedResult: DashboardVersionSpec{
|
||||
UID: "legacy-dashboard",
|
||||
Version: 3,
|
||||
ParentVersion: 2,
|
||||
},
|
||||
expectError: false,
|
||||
checkSpec: func(t *testing.T, spec any) {
|
||||
specMap := spec.(map[string]any)
|
||||
require.Equal(t, "legacy-dashboard", specMap["uid"])
|
||||
require.Equal(t, int64(3), specMap["version"])
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should handle generation 0 correctly",
|
||||
obj: &unstructured.Unstructured{
|
||||
Object: map[string]any{
|
||||
"apiVersion": dashboardv2alpha1.GroupVersion.String(),
|
||||
"metadata": map[string]any{
|
||||
"name": "zero-gen-dashboard",
|
||||
"generation": int64(0),
|
||||
},
|
||||
"spec": map[string]any{
|
||||
"title": "Zero Generation Dashboard",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedResult: DashboardVersionSpec{
|
||||
UID: "zero-gen-dashboard",
|
||||
Version: 0,
|
||||
ParentVersion: 0,
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "should handle generation 1 correctly",
|
||||
obj: &unstructured.Unstructured{
|
||||
Object: map[string]any{
|
||||
"apiVersion": "dashboard.grafana.app/v0",
|
||||
"metadata": map[string]any{
|
||||
"name": "one-gen-dashboard",
|
||||
"generation": int64(1),
|
||||
},
|
||||
"spec": map[string]any{
|
||||
"title": "One Generation Dashboard",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedResult: DashboardVersionSpec{
|
||||
UID: "one-gen-dashboard",
|
||||
Version: 1,
|
||||
ParentVersion: 0,
|
||||
},
|
||||
expectError: false,
|
||||
checkSpec: func(t *testing.T, spec any) {
|
||||
specMap := spec.(map[string]any)
|
||||
require.Equal(t, int64(1), specMap["version"])
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should return error when spec is missing for v2alpha1/v2beta1",
|
||||
obj: &unstructured.Unstructured{
|
||||
Object: map[string]any{
|
||||
"apiVersion": dashboardv2alpha1.GroupVersion.String(),
|
||||
"metadata": map[string]any{
|
||||
"name": "no-spec-dashboard",
|
||||
"generation": int64(1),
|
||||
},
|
||||
// Missing spec
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
errorMessage: "error parsing dashboard from k8s response",
|
||||
},
|
||||
{
|
||||
name: "should return error when spec is missing for legacy API",
|
||||
obj: &unstructured.Unstructured{
|
||||
Object: map[string]any{
|
||||
"apiVersion": "dashboard.grafana.app/v1",
|
||||
"metadata": map[string]any{
|
||||
"name": "no-spec-legacy-dashboard",
|
||||
"generation": int64(1),
|
||||
},
|
||||
// Missing spec
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
errorMessage: "error parsing dashboard from k8s response",
|
||||
},
|
||||
{
|
||||
name: "should return error when spec is not map for legacy API",
|
||||
obj: &unstructured.Unstructured{
|
||||
Object: map[string]any{
|
||||
"apiVersion": "dashboard.grafana.app/v1",
|
||||
"metadata": map[string]any{
|
||||
"name": "invalid-spec-dashboard",
|
||||
"generation": int64(1),
|
||||
},
|
||||
"spec": "not a map", // Invalid spec type
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
errorMessage: "error parsing dashboard from k8s response",
|
||||
},
|
||||
{
|
||||
name: "should handle edge cases correctly",
|
||||
obj: &unstructured.Unstructured{
|
||||
Object: map[string]any{
|
||||
"apiVersion": dashboardv2beta1.GroupVersion.String(),
|
||||
"metadata": map[string]any{
|
||||
"name": "high-gen-dashboard",
|
||||
"generation": int64(999999),
|
||||
},
|
||||
"spec": map[string]any{
|
||||
"title": "High Generation Dashboard",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedResult: DashboardVersionSpec{
|
||||
UID: "high-gen-dashboard",
|
||||
Version: 999999,
|
||||
ParentVersion: 999998,
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var result DashboardVersionSpec
|
||||
err := UnstructuredToDashboardVersionSpec(tt.obj, &result)
|
||||
|
||||
if tt.expectError {
|
||||
require.Error(t, err)
|
||||
require.Equal(t, tt.errorMessage, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
// Check basic fields
|
||||
require.Equal(t, tt.expectedResult.UID, result.UID)
|
||||
require.Equal(t, tt.expectedResult.Version, result.Version)
|
||||
require.Equal(t, tt.expectedResult.ParentVersion, result.ParentVersion)
|
||||
|
||||
// Check that spec is properly set
|
||||
require.NotNil(t, result.Spec)
|
||||
|
||||
// Check that MetaAccessor is properly set
|
||||
require.NotNil(t, result.MetaAccessor)
|
||||
|
||||
// Run custom spec checks if provided
|
||||
if tt.checkSpec != nil {
|
||||
tt.checkSpec(t, result.Spec)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type FakeDashboardVersionStore struct {
|
||||
ExpectedDashboardVersion *dashver.DashboardVersion
|
||||
ExptectedDeletedVersions int64
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards/database"
|
||||
dashboardservice "github.com/grafana/grafana/pkg/services/dashboards/service"
|
||||
dashclient "github.com/grafana/grafana/pkg/services/dashboards/service/client"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
"github.com/grafana/grafana/pkg/services/folder/folderimpl"
|
||||
@@ -55,13 +56,32 @@ func SetupDashboardService(tb testing.TB, sqlStore db.DB, fs *folderimpl.Dashboa
|
||||
require.NoError(tb, err)
|
||||
|
||||
dashboardService, err := dashboardservice.ProvideDashboardServiceImpl(
|
||||
cfg, dashboardStore, fs,
|
||||
features, folderPermissions, ac,
|
||||
&actest.FakeService{}, foldertest.NewFakeService(),
|
||||
nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil,
|
||||
dualwrite.ProvideTestService(), sort.ProvideService(),
|
||||
cfg,
|
||||
dashboardStore,
|
||||
fs,
|
||||
features,
|
||||
folderPermissions,
|
||||
ac,
|
||||
&actest.FakeService{},
|
||||
foldertest.NewFakeService(),
|
||||
nil,
|
||||
quotaService,
|
||||
nil,
|
||||
nil,
|
||||
dualwrite.ProvideTestService(),
|
||||
serverlock.ProvideService(sqlStore, tracing.InitializeTracerForTest()),
|
||||
kvstore.NewFakeKVStore(),
|
||||
dashclient.NewK8sClientWithFallback(
|
||||
cfg,
|
||||
client.MockTestRestConfig{},
|
||||
dashboardStore,
|
||||
nil,
|
||||
nil,
|
||||
sort.ProvideService(),
|
||||
dualwrite.ProvideTestService(),
|
||||
nil,
|
||||
features,
|
||||
),
|
||||
)
|
||||
require.NoError(tb, err)
|
||||
dashboardService.RegisterDashboardPermissions(dashboardPermissions)
|
||||
|
||||
@@ -31,6 +31,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
dashboardStore "github.com/grafana/grafana/pkg/services/dashboards/database"
|
||||
dashService "github.com/grafana/grafana/pkg/services/dashboards/service"
|
||||
dashclient "github.com/grafana/grafana/pkg/services/dashboards/service/client"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
dsservice "github.com/grafana/grafana/pkg/services/datasources/service"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
@@ -511,10 +512,34 @@ func setupEnv(t *testing.T, sqlStore db.DB, cfg *setting.Cfg, b bus.Bus, quotaSe
|
||||
nil, sqlStore, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService(), apiserver.WithoutRestConfig)
|
||||
orgService, err := orgimpl.ProvideService(sqlStore, cfg, quotaService)
|
||||
require.NoError(t, err)
|
||||
dashService, err := dashService.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuremgmt.WithFeatures(), acmock.NewMockedPermissionsService(),
|
||||
ac, actest.FakeService{}, folderSvc, nil, client.MockTestRestConfig{}, nil, quotaService, orgService, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService(),
|
||||
dashService, err := dashService.ProvideDashboardServiceImpl(
|
||||
cfg,
|
||||
dashStore,
|
||||
folderStore,
|
||||
featuremgmt.WithFeatures(),
|
||||
acmock.NewMockedPermissionsService(),
|
||||
ac,
|
||||
actest.FakeService{},
|
||||
folderSvc,
|
||||
nil,
|
||||
quotaService,
|
||||
orgService,
|
||||
nil,
|
||||
dualwrite.ProvideTestService(),
|
||||
serverlock.ProvideService(sqlStore, tracing.InitializeTracerForTest()),
|
||||
kvstore.NewFakeKVStore())
|
||||
kvstore.NewFakeKVStore(),
|
||||
dashclient.NewK8sClientWithFallback(
|
||||
cfg,
|
||||
client.MockTestRestConfig{},
|
||||
dashStore,
|
||||
nil,
|
||||
nil,
|
||||
sort.ProvideService(),
|
||||
dualwrite.ProvideTestService(),
|
||||
nil,
|
||||
featuremgmt.WithFeatures(),
|
||||
),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
dashService.RegisterDashboardPermissions(acmock.NewMockedPermissionsService())
|
||||
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
|
||||
|
||||
Reference in New Issue
Block a user