From e25d09ff3e1fac02c469ddd7b263458a0ce0b757 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Mon, 12 Jan 2026 18:08:56 +0300 Subject: [PATCH] support generic response --- pkg/registry/apis/datasource/register.go | 11 ++++-- pkg/storage/unified/apistore/prepare.go | 41 +++++++++++++++++--- pkg/storage/unified/apistore/prepare_test.go | 2 + pkg/storage/unified/apistore/store.go | 2 + pkg/tests/apis/datasource/testdata_test.go | 2 +- 5 files changed, 48 insertions(+), 10 deletions(-) diff --git a/pkg/registry/apis/datasource/register.go b/pkg/registry/apis/datasource/register.go index 19ed5c4f028..c94bdf8d063 100644 --- a/pkg/registry/apis/datasource/register.go +++ b/pkg/registry/apis/datasource/register.go @@ -30,6 +30,7 @@ import ( "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/storage/unified/apistore" "github.com/grafana/grafana/pkg/tsdb/grafana-testdata-datasource/kinds" ) @@ -89,10 +90,6 @@ func RegisterAPIService( return nil, fmt.Errorf("plugin client is not a PluginClient: %T", pluginClient) } - if pluginJSON.ID != "grafana-testdata-datasource" { - continue // FOR TESTING! codec currently picks the first registered apiVersion :cry: - } - builder, err = NewDataSourceAPIBuilder( pluginJSON, client, @@ -223,6 +220,12 @@ func (b *DataSourceAPIBuilder) AllowedV0Alpha1Resources() []string { } func (b *DataSourceAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error { + opts.StorageOptsRegister(b.datasourceResourceInfo.GroupResource(), apistore.StorageOptions{ + EnableFolderSupport: false, + + Scheme: opts.Scheme, // allows for generic Type applied to multiple groups + }) + storage := map[string]rest.Storage{} // Register the raw datasource connection diff --git a/pkg/storage/unified/apistore/prepare.go b/pkg/storage/unified/apistore/prepare.go index 3691cb29bbd..40857b456f3 100644 --- a/pkg/storage/unified/apistore/prepare.go +++ b/pkg/storage/unified/apistore/prepare.go @@ -3,8 +3,10 @@ package apistore import ( "bytes" "context" + "encoding/json" "errors" "fmt" + "io" "time" "github.com/dustin/go-humanize" @@ -138,8 +140,7 @@ func (s *Storage) prepareObjectForStorage(ctx context.Context, newObject runtime return v, err } - err = s.codec.Encode(newObject, &v.raw) - if err == nil { + if err = s.encode(newObject, &v.raw); err == nil { err = s.handleLargeResources(ctx, obj, &v.raw) } return v, err @@ -233,8 +234,7 @@ func (s *Storage) prepareObjectForUpdate(ctx context.Context, updateObject runti obj.SetAnnotation(utils.AnnoKeyUpdatedTimestamp, previous.GetAnnotation(utils.AnnoKeyUpdatedTimestamp)) } - err = s.codec.Encode(updateObject, &v.raw) - if err == nil { + if s.encode(updateObject, &v.raw); err == nil { err = s.handleLargeResources(ctx, obj, &v.raw) } return v, err @@ -268,7 +268,38 @@ func (s *Storage) handleLargeResources(ctx context.Context, obj utils.GrafanaMet } // Now encode the smaller version - return s.codec.Encode(orig, buf) + return s.encode(orig, buf) } return nil } + +func (s *Storage) encode(obj runtime.Object, w io.Writer) error { + // The standard encoder is fine when only one type maps to a group + if s.opts.Scheme == nil { + return s.codec.Encode(obj, w) + } + + // Ensure group+version+kind are configured + info := obj.GetObjectKind() + gvk := info.GroupVersionKind() + if gvk.Group == "" || gvk.Kind == "" || gvk.Version == "" { + gvks, _, err := s.opts.Scheme.ObjectKinds(obj) + if err != nil { + return fmt.Errorf("unable to encode object %w", err) + } + for _, v := range gvks { + if v.Group != s.gr.Group { + continue // skip values not in this group + } + gvk.Group = v.Group + gvk.Kind = v.Kind + if gvk.Version == "" { + gvk.Version = v.Version + } + info.SetGroupVersionKind(gvk) + break + } + } + + return json.NewEncoder(w).Encode(obj) +} diff --git a/pkg/storage/unified/apistore/prepare_test.go b/pkg/storage/unified/apistore/prepare_test.go index a35c2398736..ff291dec185 100644 --- a/pkg/storage/unified/apistore/prepare_test.go +++ b/pkg/storage/unified/apistore/prepare_test.go @@ -33,9 +33,11 @@ func TestPrepareObjectForStorage(t *testing.T) { node, err := snowflake.NewNode(rand.Int64N(1024)) require.NoError(t, err) s := &Storage{ + gr: dashv1.DashboardResourceInfo.GroupResource(), codec: apitesting.TestCodec(rtcodecs, dashv1.DashboardResourceInfo.GroupVersion()), snowflake: node, opts: StorageOptions{ + Scheme: rtscheme, EnableFolderSupport: true, LargeObjectSupport: nil, MaximumNameLength: 100, diff --git a/pkg/storage/unified/apistore/store.go b/pkg/storage/unified/apistore/store.go index 5adf5e8b958..096c0ff4c2d 100644 --- a/pkg/storage/unified/apistore/store.go +++ b/pkg/storage/unified/apistore/store.go @@ -57,6 +57,8 @@ type DefaultPermissionSetter = func(ctx context.Context, key *resourcepb.Resourc // Optional settings that apply to a single resource type StorageOptions struct { + Scheme *runtime.Scheme + // ????: should we constrain this to only dashboards for now? // Not yet clear if this is a good general solution, or just a stop-gap LargeObjectSupport LargeObjectSupport diff --git a/pkg/tests/apis/datasource/testdata_test.go b/pkg/tests/apis/datasource/testdata_test.go index 857b12f7d9d..15fee52655a 100644 --- a/pkg/tests/apis/datasource/testdata_test.go +++ b/pkg/tests/apis/datasource/testdata_test.go @@ -37,7 +37,7 @@ func TestIntegrationTestDatasource(t *testing.T) { grafanarest.Mode0, // Legacy only // grafanarest.Mode2, // write both, read legacy // grafanarest.Mode3, // write both, read unified - // grafanarest.Mode5, // Unified only + grafanarest.Mode5, // Unified only } { t.Run(fmt.Sprintf("testdata (mode:%d)", mode), func(t *testing.T) { ctx := context.Background()