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:
Ryan McKinley 2024-12-20 22:33:49 +03:00 committed by GitHub
parent 54333473f7
commit 1a46039037
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 240 additions and 9 deletions

View File

@ -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]

View File

@ -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{

View 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")
}

View 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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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()

View File

@ -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