diff --git a/pkg/apimachinery/utils/meta.go b/pkg/apimachinery/utils/meta.go index 403ef6d2125..a64d085912e 100644 --- a/pkg/apimachinery/utils/meta.go +++ b/pkg/apimachinery/utils/meta.go @@ -34,6 +34,9 @@ const AnnoKeyRepoPath = "grafana.app/repoPath" const AnnoKeyRepoHash = "grafana.app/repoHash" const AnnoKeyRepoTimestamp = "grafana.app/repoTimestamp" +// Deprecated: will be removed in grafana 13 +const labelKeyDeprecatedInternalID = "grafana.app/deprecatedInternalID" + // These can be removed once we verify that non of the dual-write sources // (for dashboards/playlists/etc) depend on the saved internal ID in SQL const oldAnnoKeyOriginName = "grafana.app/originName" @@ -102,6 +105,12 @@ type GrafanaMetaAccessor interface { SetBlob(v *BlobInfo) GetBlob() *BlobInfo + // Deprecated: This will be removed in Grafana 13 + GetDeprecatedInternalID() int64 + + // Deprecated: This will be removed in Grafana 13 + SetDeprecatedInternalID(id int64) + GetRepositoryInfo() (*ResourceRepositoryInfo, error) SetRepositoryInfo(info *ResourceRepositoryInfo) GetRepositoryName() string @@ -283,6 +292,44 @@ func (m *grafanaMetaAccessor) SetSlug(v string) { m.SetAnnotation(AnnoKeySlug, v) } +// This will be removed in Grafana 13. Do not add any new usage of it. +func (m *grafanaMetaAccessor) GetDeprecatedInternalID() int64 { + labels := m.obj.GetLabels() + if labels == nil { + return 0 + } + + if internalID, ok := labels[labelKeyDeprecatedInternalID]; ok { + id, err := strconv.ParseInt(internalID, 10, 64) + if err == nil { + return id + } + } + + return 0 +} + +// This will be removed in Grafana 13. Do not add any new usage of it. +func (m *grafanaMetaAccessor) SetDeprecatedInternalID(id int64) { + labels := m.obj.GetLabels() + + // disallow setting it to 0 + if id == 0 { + if labels != nil { + delete(labels, labelKeyDeprecatedInternalID) + m.obj.SetLabels(labels) + } + return + } + + if labels == nil { + labels = make(map[string]string) + } + + labels[labelKeyDeprecatedInternalID] = strconv.FormatInt(id, 10) + m.obj.SetLabels(labels) +} + // This allows looking up a primary and secondary key -- if either exist the value will be returned func (m *grafanaMetaAccessor) getAnnoValue(primary, secondary string) (string, bool) { v, ok := m.obj.GetAnnotations()[primary] diff --git a/pkg/apimachinery/utils/meta_test.go b/pkg/apimachinery/utils/meta_test.go index 051e4a8bf87..451deaa47d5 100644 --- a/pkg/apimachinery/utils/meta_test.go +++ b/pkg/apimachinery/utils/meta_test.go @@ -154,6 +154,28 @@ func TestMetaAccessor(t *testing.T) { require.NoError(t, err) // Must be a pointer }) + t.Run("get and set grafana labels (unstructured)", func(t *testing.T) { + res := &unstructured.Unstructured{ + Object: map[string]any{}, + } + meta, err := utils.MetaAccessor(res) + require.NoError(t, err) + + // should return 0 when not set + require.Equal(t, meta.GetDeprecatedInternalID(), int64(0)) + + // 0 is not allowed + meta.SetDeprecatedInternalID(0) + require.Equal(t, map[string]string(nil), res.GetLabels()) + + // should be able to set and get + meta.SetDeprecatedInternalID(1) + require.Equal(t, map[string]string{ + "grafana.app/deprecatedInternalID": "1", + }, res.GetLabels()) + require.Equal(t, meta.GetDeprecatedInternalID(), int64(1)) + }) + t.Run("get and set grafana metadata (unstructured)", func(t *testing.T) { // Error reading spec+status when missing res := &unstructured.Unstructured{ diff --git a/pkg/registry/apis/dashboard/legacy/client.go b/pkg/registry/apis/dashboard/legacy/client.go new file mode 100644 index 00000000000..4656fe49241 --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/client.go @@ -0,0 +1,93 @@ +package legacy + +import ( + "context" + "fmt" + + "google.golang.org/grpc" + + "github.com/grafana/grafana/pkg/storage/unified/resource" +) + +var ( + _ resource.ResourceClient = (*directResourceClient)(nil) +) + +// The direct client passes requests directly to the server using the *same* context +func NewDirectResourceClient(server resource.ResourceServer) resource.ResourceClient { + return &directResourceClient{server} +} + +type directResourceClient struct { + server resource.ResourceServer +} + +// Create implements ResourceClient. +func (d *directResourceClient) Create(ctx context.Context, in *resource.CreateRequest, opts ...grpc.CallOption) (*resource.CreateResponse, error) { + return d.server.Create(ctx, in) +} + +// Delete implements ResourceClient. +func (d *directResourceClient) Delete(ctx context.Context, in *resource.DeleteRequest, opts ...grpc.CallOption) (*resource.DeleteResponse, error) { + return d.server.Delete(ctx, in) +} + +// GetBlob implements ResourceClient. +func (d *directResourceClient) GetBlob(ctx context.Context, in *resource.GetBlobRequest, opts ...grpc.CallOption) (*resource.GetBlobResponse, error) { + return d.server.GetBlob(ctx, in) +} + +// GetStats implements ResourceClient. +func (d *directResourceClient) GetStats(ctx context.Context, in *resource.ResourceStatsRequest, opts ...grpc.CallOption) (*resource.ResourceStatsResponse, error) { + return d.server.GetStats(ctx, in) +} + +// History implements ResourceClient. +func (d *directResourceClient) History(ctx context.Context, in *resource.HistoryRequest, opts ...grpc.CallOption) (*resource.HistoryResponse, error) { + return d.server.History(ctx, in) +} + +// IsHealthy implements ResourceClient. +func (d *directResourceClient) IsHealthy(ctx context.Context, in *resource.HealthCheckRequest, opts ...grpc.CallOption) (*resource.HealthCheckResponse, error) { + return d.server.IsHealthy(ctx, in) +} + +// List implements ResourceClient. +func (d *directResourceClient) List(ctx context.Context, in *resource.ListRequest, opts ...grpc.CallOption) (*resource.ListResponse, error) { + return d.server.List(ctx, in) +} + +// Origin implements ResourceClient. +func (d *directResourceClient) Origin(ctx context.Context, in *resource.OriginRequest, opts ...grpc.CallOption) (*resource.OriginResponse, error) { + return d.server.Origin(ctx, in) +} + +// PutBlob implements ResourceClient. +func (d *directResourceClient) PutBlob(ctx context.Context, in *resource.PutBlobRequest, opts ...grpc.CallOption) (*resource.PutBlobResponse, error) { + return d.server.PutBlob(ctx, in) +} + +// Read implements ResourceClient. +func (d *directResourceClient) Read(ctx context.Context, in *resource.ReadRequest, opts ...grpc.CallOption) (*resource.ReadResponse, error) { + return d.server.Read(ctx, in) +} + +// Restore implements ResourceClient. +func (d *directResourceClient) Restore(ctx context.Context, in *resource.RestoreRequest, opts ...grpc.CallOption) (*resource.RestoreResponse, error) { + return d.server.Restore(ctx, in) +} + +// Search implements ResourceClient. +func (d *directResourceClient) Search(ctx context.Context, in *resource.ResourceSearchRequest, opts ...grpc.CallOption) (*resource.ResourceSearchResponse, error) { + return d.server.Search(ctx, in) +} + +// Update implements ResourceClient. +func (d *directResourceClient) Update(ctx context.Context, in *resource.UpdateRequest, opts ...grpc.CallOption) (*resource.UpdateResponse, error) { + return d.server.Update(ctx, in) +} + +// Watch implements ResourceClient. +func (d *directResourceClient) Watch(ctx context.Context, in *resource.WatchRequest, opts ...grpc.CallOption) (resource.ResourceStore_WatchClient, error) { + return nil, fmt.Errorf("watch not yet supported with direct resource client") +} diff --git a/pkg/registry/apis/dashboard/legacy/context.go b/pkg/registry/apis/dashboard/legacy/context.go new file mode 100644 index 00000000000..e654ee968b2 --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/context.go @@ -0,0 +1,21 @@ +package legacy + +import ( + "context" +) + +type LegacyValue struct { + DashboardID int64 +} + +type accessKey struct{} + +// WithRequester attaches the requester to the context. +func WithLegacyAccess(ctx context.Context) context.Context { + return context.WithValue(ctx, accessKey{}, &LegacyValue{}) +} + +func GetLegacyAccess(ctx context.Context) *LegacyValue { + v, _ := ctx.Value(accessKey{}).(*LegacyValue) + return v // nil if missing +} diff --git a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go index 7d4635b1db3..9e5ac127fad 100644 --- a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go +++ b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go @@ -11,6 +11,7 @@ import ( "time" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/utils/ptr" "github.com/grafana/authlib/claims" @@ -407,6 +408,15 @@ func (a *dashboardSqlAccess) SaveDashboard(ctx context.Context, orgId int64, das created = (out.Created.Unix() == out.Updated.Unix()) // and now? } dash, _, err = a.GetDashboard(ctx, orgId, out.UID, 0) + + // stash the raw value in context (if requested) + access := GetLegacyAccess(ctx) + if access != nil { + id, ok, _ := unstructured.NestedInt64(dash.Spec.Object, "id") + if ok { + access.DashboardID = id + } + } return dash, created, err } diff --git a/pkg/registry/apis/dashboard/legacy_storage.go b/pkg/registry/apis/dashboard/legacy_storage.go index b6c91fb31bf..214c2ad2202 100644 --- a/pkg/registry/apis/dashboard/legacy_storage.go +++ b/pkg/registry/apis/dashboard/legacy_storage.go @@ -1,9 +1,13 @@ package dashboard import ( + "context" + "github.com/prometheus/client_golang/prometheus" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/generic" + "k8s.io/apiserver/pkg/registry/generic/registry" "k8s.io/apiserver/pkg/registry/rest" "github.com/grafana/grafana/pkg/apimachinery/utils" @@ -28,11 +32,6 @@ func (s *DashboardStorage) NewStore(scheme *runtime.Scheme, defaultOptsGetter ge server, err := resource.NewResourceServer(resource.ResourceServerOptions{ Backend: s.Access, Reg: reg, - // WriteAccess: resource.WriteAccessHooks{ - // Folder: func(ctx context.Context, user identity.Requester, uid string) bool { - // // ??? - // }, - // }, }) if err != nil { return nil, err @@ -44,9 +43,47 @@ func (s *DashboardStorage) NewStore(scheme *runtime.Scheme, defaultOptsGetter ge if err != nil { return nil, err } - client := resource.NewLocalResourceClient(server) + client := legacy.NewDirectResourceClient(server) // same context optsGetter := apistore.NewRESTOptionsGetterForClient(client, defaultOpts.StorageConfig.Config, ) - return grafanaregistry.NewRegistryStore(scheme, resourceInfo, optsGetter) + + store, err := grafanaregistry.NewRegistryStore(scheme, resourceInfo, optsGetter) + return &storeWrapper{ + Store: store, + }, err +} + +type storeWrapper struct { + *registry.Store +} + +// Create will create the dashboard using legacy storage and make sure the internal ID is set on the return object +func (s *storeWrapper) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) { + ctx = legacy.WithLegacyAccess(ctx) + obj, err := s.Store.Create(ctx, obj, createValidation, options) + access := legacy.GetLegacyAccess(ctx) + if access != nil && access.DashboardID > 0 { + meta, _ := utils.MetaAccessor(obj) + if meta != nil { + // skip the linter error for deprecated function + meta.SetDeprecatedInternalID(access.DashboardID) //nolint:staticcheck + } + } + return obj, err +} + +// Update will update the dashboard using legacy storage and make sure the internal ID is set on the return object +func (s *storeWrapper) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) { + ctx = legacy.WithLegacyAccess(ctx) + obj, created, err := s.Store.Update(ctx, name, objInfo, createValidation, updateValidation, forceAllowCreate, options) + access := legacy.GetLegacyAccess(ctx) + if access != nil && access.DashboardID > 0 { + meta, _ := utils.MetaAccessor(obj) + if meta != nil { + // skip the linter error for deprecated function + meta.SetDeprecatedInternalID(access.DashboardID) //nolint:staticcheck + } + } + return obj, created, err } diff --git a/pkg/storage/unified/apistore/prepare.go b/pkg/storage/unified/apistore/prepare.go index 8bad6bfbe0a..3547dbf9873 100644 --- a/pkg/storage/unified/apistore/prepare.go +++ b/pkg/storage/unified/apistore/prepare.go @@ -114,7 +114,8 @@ func (s *Storage) prepareObjectForUpdate(ctx context.Context, updateObject runti obj.SetCreatedBy(previous.GetCreatedBy()) obj.SetCreationTimestamp(previous.GetCreationTimestamp()) - obj.SetResourceVersion("") // removed from saved JSON because the RV is not yet calculated + obj.SetResourceVersion("") // removed from saved JSON because the RV is not yet calculated + obj.SetDeprecatedInternalID(previous.GetDeprecatedInternalID()) // nolint:staticcheck // Read+write will verify that origin format is accurate repo, err := obj.GetRepositoryInfo() diff --git a/pkg/tests/apis/dashboard/testdata/dashboard-generate.yaml b/pkg/tests/apis/dashboard/testdata/dashboard-generate.yaml index d5dfcb78564..2b8f4f60be6 100644 --- a/pkg/tests/apis/dashboard/testdata/dashboard-generate.yaml +++ b/pkg/tests/apis/dashboard/testdata/dashboard-generate.yaml @@ -3,4 +3,4 @@ kind: Dashboard metadata: generateName: x # anything is ok here... except yes or true -- they become boolean! spec: - title: Dashboard with auto generated UID ${NOW} \ No newline at end of file + title: Dashboard with auto generated name \ No newline at end of file