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 AnnoKeyRepoHash = "grafana.app/repoHash"
|
||||||
const AnnoKeyRepoTimestamp = "grafana.app/repoTimestamp"
|
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
|
// 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
|
// (for dashboards/playlists/etc) depend on the saved internal ID in SQL
|
||||||
const oldAnnoKeyOriginName = "grafana.app/originName"
|
const oldAnnoKeyOriginName = "grafana.app/originName"
|
||||||
@ -102,6 +105,12 @@ type GrafanaMetaAccessor interface {
|
|||||||
SetBlob(v *BlobInfo)
|
SetBlob(v *BlobInfo)
|
||||||
GetBlob() *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)
|
GetRepositoryInfo() (*ResourceRepositoryInfo, error)
|
||||||
SetRepositoryInfo(info *ResourceRepositoryInfo)
|
SetRepositoryInfo(info *ResourceRepositoryInfo)
|
||||||
GetRepositoryName() string
|
GetRepositoryName() string
|
||||||
@ -283,6 +292,44 @@ func (m *grafanaMetaAccessor) SetSlug(v string) {
|
|||||||
m.SetAnnotation(AnnoKeySlug, v)
|
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
|
// 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) {
|
func (m *grafanaMetaAccessor) getAnnoValue(primary, secondary string) (string, bool) {
|
||||||
v, ok := m.obj.GetAnnotations()[primary]
|
v, ok := m.obj.GetAnnotations()[primary]
|
||||||
|
@ -154,6 +154,28 @@ func TestMetaAccessor(t *testing.T) {
|
|||||||
require.NoError(t, err) // Must be a pointer
|
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) {
|
t.Run("get and set grafana metadata (unstructured)", func(t *testing.T) {
|
||||||
// Error reading spec+status when missing
|
// Error reading spec+status when missing
|
||||||
res := &unstructured.Unstructured{
|
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"
|
"time"
|
||||||
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/utils/ptr"
|
"k8s.io/utils/ptr"
|
||||||
|
|
||||||
"github.com/grafana/authlib/claims"
|
"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?
|
created = (out.Created.Unix() == out.Updated.Unix()) // and now?
|
||||||
}
|
}
|
||||||
dash, _, err = a.GetDashboard(ctx, orgId, out.UID, 0)
|
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
|
return dash, created, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
package dashboard
|
package dashboard
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apiserver/pkg/registry/generic"
|
"k8s.io/apiserver/pkg/registry/generic"
|
||||||
|
"k8s.io/apiserver/pkg/registry/generic/registry"
|
||||||
"k8s.io/apiserver/pkg/registry/rest"
|
"k8s.io/apiserver/pkg/registry/rest"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
"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{
|
server, err := resource.NewResourceServer(resource.ResourceServerOptions{
|
||||||
Backend: s.Access,
|
Backend: s.Access,
|
||||||
Reg: reg,
|
Reg: reg,
|
||||||
// WriteAccess: resource.WriteAccessHooks{
|
|
||||||
// Folder: func(ctx context.Context, user identity.Requester, uid string) bool {
|
|
||||||
// // ???
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -44,9 +43,47 @@ func (s *DashboardStorage) NewStore(scheme *runtime.Scheme, defaultOptsGetter ge
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
client := resource.NewLocalResourceClient(server)
|
client := legacy.NewDirectResourceClient(server) // same context
|
||||||
optsGetter := apistore.NewRESTOptionsGetterForClient(client,
|
optsGetter := apistore.NewRESTOptionsGetterForClient(client,
|
||||||
defaultOpts.StorageConfig.Config,
|
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.SetCreatedBy(previous.GetCreatedBy())
|
||||||
obj.SetCreationTimestamp(previous.GetCreationTimestamp())
|
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
|
// Read+write will verify that origin format is accurate
|
||||||
repo, err := obj.GetRepositoryInfo()
|
repo, err := obj.GetRepositoryInfo()
|
||||||
|
@ -3,4 +3,4 @@ kind: Dashboard
|
|||||||
metadata:
|
metadata:
|
||||||
generateName: x # anything is ok here... except yes or true -- they become boolean!
|
generateName: x # anything is ok here... except yes or true -- they become boolean!
|
||||||
spec:
|
spec:
|
||||||
title: Dashboard with auto generated UID ${NOW}
|
title: Dashboard with auto generated name
|
Loading…
Reference in New Issue
Block a user