mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
K8s/Dashboards: Pass the legacy internal ID into labels (#98311)
--------- Co-authored-by: Stephanie Hingtgen <stephanie.hingtgen@grafana.com> Co-authored-by: Todd Treece <360020+toddtreece@users.noreply.github.com>
This commit is contained in:
parent
54333473f7
commit
1a46039037
@ -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]
|
||||
|
@ -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{
|
||||
|
93
pkg/registry/apis/dashboard/legacy/client.go
Normal file
93
pkg/registry/apis/dashboard/legacy/client.go
Normal file
@ -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")
|
||||
}
|
21
pkg/registry/apis/dashboard/legacy/context.go
Normal file
21
pkg/registry/apis/dashboard/legacy/context.go
Normal file
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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}
|
||||
title: Dashboard with auto generated name
|
Loading…
Reference in New Issue
Block a user