now with dashboard history

This commit is contained in:
Ryan McKinley 2024-06-27 01:20:29 +03:00
parent 7345ece8ef
commit 999cd506c0
9 changed files with 140 additions and 53 deletions

View File

@ -19,6 +19,7 @@ import (
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils"
"github.com/grafana/grafana/pkg/services/dashboards"
dashver "github.com/grafana/grafana/pkg/services/dashboardversion"
"github.com/grafana/grafana/pkg/services/provisioning"
"github.com/grafana/grafana/pkg/services/sqlstore/session"
"github.com/grafana/grafana/pkg/storage/unified/resource"
@ -55,19 +56,25 @@ type dashboardSqlAccess struct {
namespacer request.NamespaceMapper
dashStore dashboards.Store
provisioning provisioning.ProvisioningService
versions dashver.Service
// Typically one... the server wrapper
subscribers []chan *resource.WrittenEvent
mutex sync.Mutex
}
func NewDashboardAccess(sql db.DB, namespacer request.NamespaceMapper, dashStore dashboards.Store, provisioning provisioning.ProvisioningService) DashboardAccess {
func NewDashboardAccess(sql db.DB,
namespacer request.NamespaceMapper,
dashStore dashboards.Store,
provisioning provisioning.ProvisioningService,
versions dashver.Service) DashboardAccess {
return &dashboardSqlAccess{
sql: sql,
sess: sql.GetSqlxSession(),
namespacer: namespacer,
dashStore: dashStore,
provisioning: provisioning,
versions: versions,
}
}

View File

@ -6,9 +6,12 @@ import (
"fmt"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/grafana/grafana/pkg/apimachinery/utils"
dashboard "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
dashver "github.com/grafana/grafana/pkg/services/dashboardversion"
"github.com/grafana/grafana/pkg/storage/unified/resource"
)
@ -241,7 +244,7 @@ func (a *dashboardSqlAccess) SupportsSignedURLs() bool {
}
func (a *dashboardSqlAccess) PutBlob(context.Context, *resource.PutBlobRequest) (*resource.PutBlobResponse, error) {
return nil, fmt.Errorf("not implemented yet")
return nil, fmt.Errorf("put blob not implemented yet")
}
func (a *dashboardSqlAccess) GetBlob(ctx context.Context, key *resource.ResourceKey, info *utils.BlobInfo, mustProxy bool) (*resource.GetBlobResponse, error) {
@ -262,3 +265,51 @@ func (a *dashboardSqlAccess) GetBlob(ctx context.Context, key *resource.Resource
rsp.Value, err = json.Marshal(dash.Spec)
return rsp, err
}
func (a *dashboardSqlAccess) History(ctx context.Context, req *resource.HistoryRequest) (*resource.HistoryResponse, error) {
ns, err := request.ParseNamespace(req.Key.Namespace)
if err == nil {
err = isDashboardKey(req.Key, true)
}
if err != nil {
return nil, err
}
versions, err := a.versions.List(ctx, &dashver.ListDashboardVersionsQuery{
OrgID: ns.OrgID,
DashboardUID: req.Key.Name,
Limit: 100,
})
if err != nil {
return nil, err
}
rsp := &resource.HistoryResponse{}
for _, version := range versions {
partial := &metav1.PartialObjectMetadata{}
meta, err := utils.MetaAccessor(partial)
if err != nil {
return nil, err
}
meta.SetName(version.DashboardUID)
meta.SetCreationTimestamp(metav1.NewTime(version.Created)) // ???
meta.SetUpdatedTimestampMillis(version.Created.UnixMilli())
meta.SetMessage(version.Message)
meta.SetResourceVersionInt64(version.Created.UnixMilli())
bytes, err := json.Marshal(partial)
if err != nil {
return nil, err
}
rsp.Items = append(rsp.Items, &resource.ResourceMeta{
ResourceVersion: version.Created.UnixMilli(),
PartialObjectMeta: bytes,
})
}
return rsp, err
}
// Used for efficient provisioning
func (a *dashboardSqlAccess) Origin(context.Context, *resource.OriginRequest) (*resource.OriginResponse, error) {
return nil, fmt.Errorf("not yet (origin)")
}

View File

@ -23,6 +23,7 @@ type DashboardQuery struct {
type DashboardAccess interface {
resource.AppendingStore
resource.BlobStore
resource.ResourceSearchServer
GetDashboard(ctx context.Context, orgId int64, uid string) (*dashboardsV0.Dashboard, int64, error)

View File

@ -17,12 +17,15 @@ type dashboardStorage struct {
resource common.ResourceInfo
access access.DashboardAccess
tableConverter rest.TableConvertor
server resource.ResourceServer
}
func (s *dashboardStorage) newStore(scheme *runtime.Scheme, defaultOptsGetter generic.RESTOptionsGetter) (rest.Storage, error) {
server, err := resource.NewResourceServer(resource.ResourceServerOptions{
Store: s.access,
Blob: s.access,
Store: s.access,
Search: s.access,
Blob: s.access,
// WriteAccess: resource.WriteAccessHooks{
// Folder: func(ctx context.Context, user identity.Requester, uid string) bool {
// // ???
@ -32,6 +35,7 @@ func (s *dashboardStorage) newStore(scheme *runtime.Scheme, defaultOptsGetter ge
if err != nil {
return nil, err
}
s.server = server
resourceInfo := s.resource
defaultOpts, err := defaultOptsGetter.GetRESTOptions(resourceInfo.GroupResource())

View File

@ -39,9 +39,8 @@ var _ builder.APIGroupBuilder = (*DashboardsAPIBuilder)(nil)
type DashboardsAPIBuilder struct {
dashboardService dashboards.DashboardService
dashboardVersionService dashver.Service
accessControl accesscontrol.AccessControl
store *dashboardStorage
accessControl accesscontrol.AccessControl
store *dashboardStorage
log log.Logger
}
@ -65,13 +64,12 @@ func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles,
builder := &DashboardsAPIBuilder{
log: log.New("grafana-apiserver.dashboards"),
dashboardService: dashboardService,
dashboardVersionService: dashboardVersionService,
accessControl: accessControl,
dashboardService: dashboardService,
accessControl: accessControl,
store: &dashboardStorage{
resource: dashboard.DashboardResourceInfo,
access: access.NewDashboardAccess(sql, namespacer, dashStore, provisioning),
access: access.NewDashboardAccess(sql, namespacer, dashStore, provisioning, dashboardVersionService),
tableConverter: gapiutil.NewTableConverter(
dashboard.DashboardResourceInfo.GroupResource(),
[]metav1.TableColumnDefinition{
@ -114,6 +112,8 @@ func addKnownTypes(scheme *runtime.Scheme, gv schema.GroupVersion) {
&v0alpha1.DashboardWithAccessInfo{},
&v0alpha1.DashboardVersionList{},
&v0alpha1.VersionsQueryOptions{},
&metav1.PartialObjectMetadata{},
&metav1.PartialObjectMetadataList{},
)
}
@ -158,7 +158,7 @@ func (b *DashboardsAPIBuilder) GetAPIGroupInfo(
builder: b,
}
storage[dash.StoragePath("versions")] = &VersionsREST{
builder: b,
search: b.store.server, // resource.NewLocalResourceSearchClient(b.store.server),
}
// // Dual writes if a RESTOptionsGetter is provided

View File

@ -2,7 +2,7 @@ package dashboard
import (
"context"
"fmt"
"encoding/json"
"net/http"
"strconv"
"strings"
@ -11,21 +11,21 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/registry/rest"
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
"github.com/grafana/grafana/pkg/apimachinery/utils"
dashboard "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
dashver "github.com/grafana/grafana/pkg/services/dashboardversion"
"github.com/grafana/grafana/pkg/storage/unified/resource"
)
type VersionsREST struct {
builder *DashboardsAPIBuilder
search resource.ResourceSearchServer // should be a client!
}
var _ = rest.Connecter(&VersionsREST{})
var _ = rest.StorageMetadata(&VersionsREST{})
func (r *VersionsREST) New() runtime.Object {
return &dashboard.DashboardVersionList{}
return &metav1.PartialObjectMetadataList{}
}
func (r *VersionsREST) Destroy() {
@ -40,7 +40,7 @@ func (r *VersionsREST) ProducesMIMETypes(verb string) []string {
}
func (r *VersionsREST) ProducesObject(verb string) interface{} {
return &dashboard.DashboardVersionList{}
return &metav1.PartialObjectMetadataList{}
}
func (r *VersionsREST) NewConnectOptions() (runtime.Object, bool, string) {
@ -52,66 +52,74 @@ func (r *VersionsREST) Connect(ctx context.Context, uid string, opts runtime.Obj
if err != nil {
return nil, err
}
key := &resource.ResourceKey{
Namespace: info.Value,
Group: dashboard.GROUP,
Resource: dashboard.DashboardResourceInfo.GroupResource().Resource,
Name: uid,
}
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
path := req.URL.Path
idx := strings.LastIndex(path, "/versions/")
if idx > 0 {
key := path[strings.LastIndex(path, "/")+1:]
version, err := strconv.Atoi(key)
vkey := path[strings.LastIndex(path, "/")+1:]
version, err := strconv.ParseInt(vkey, 10, 64)
if err != nil {
responder.Error(err)
return
}
dto, err := r.builder.dashboardVersionService.Get(ctx, &dashver.GetDashboardVersionQuery{
DashboardUID: uid,
OrgID: info.OrgID,
Version: version,
dashbytes, err := r.search.Read(ctx, &resource.ReadRequest{
Key: key,
ResourceVersion: version,
})
if err != nil {
responder.Error(err)
return
}
data, _ := dto.Data.Map()
// Convert the version to a regular dashboard
dash := &dashboard.Dashboard{
ObjectMeta: metav1.ObjectMeta{
Name: uid,
CreationTimestamp: metav1.NewTime(dto.Created),
},
Spec: common.Unstructured{Object: data},
dash := &dashboard.Dashboard{}
json.Unmarshal(dashbytes.Value, dash)
meta, err := utils.MetaAccessor(dash)
if err != nil {
responder.Error(err)
return
}
meta.SetResourceVersionInt64(dashbytes.ResourceVersion)
responder.Object(100, dash)
return
}
// Or list versions
rsp, err := r.builder.dashboardVersionService.List(ctx, &dashver.ListDashboardVersionsQuery{
DashboardUID: uid,
OrgID: info.OrgID,
rsp, err := r.search.History(ctx, &resource.HistoryRequest{
NextPageToken: "", // TODO!
Limit: 100,
Key: key,
})
if err != nil {
responder.Error(err)
return
}
versions := &dashboard.DashboardVersionList{}
for _, v := range rsp {
info := dashboard.DashboardVersionInfo{
Version: v.Version,
Created: v.Created.UnixMilli(),
Message: v.Message,
}
if v.ParentVersion != v.Version {
info.ParentVersion = v.ParentVersion
}
if v.CreatedBy > 0 {
info.CreatedBy = fmt.Sprintf("%d", v.CreatedBy)
}
versions.Items = append(versions.Items, info)
list := &metav1.PartialObjectMetadataList{
ListMeta: metav1.ListMeta{
Continue: rsp.NextPageToken,
},
}
responder.Object(http.StatusOK, versions)
if rsp.ResourceVersion > 0 {
list.ResourceVersion = strconv.FormatInt(rsp.ResourceVersion, 10)
}
for _, v := range rsp.Items {
partial := metav1.PartialObjectMetadata{}
err = json.Unmarshal(v.PartialObjectMeta, &partial)
if err != nil {
responder.Error(err)
return
}
list.Items = append(list.Items, partial)
}
responder.Object(http.StatusOK, list)
}), nil
}

View File

@ -25,6 +25,22 @@ func NewLocalResourceStoreClient(server ResourceStoreServer) ResourceStoreClient
return NewResourceStoreClient(grpchan.InterceptClientConn(channel, grpcUtils.UnaryClientInterceptor, grpcUtils.StreamClientInterceptor))
}
func NewLocalResourceSearchClient(server ResourceStoreServer) ResourceSearchClient {
channel := &inprocgrpc.Channel{}
auth := &grpcUtils.Authenticator{}
channel.RegisterService(
grpchan.InterceptServer(
&ResourceStore_ServiceDesc,
grpcAuth.UnaryServerInterceptor(auth.Authenticate),
grpcAuth.StreamServerInterceptor(auth.Authenticate),
),
server,
)
return NewResourceSearchClient(grpchan.InterceptClientConn(channel, grpcUtils.UnaryClientInterceptor, grpcUtils.StreamClientInterceptor))
}
func NewResourceStoreClientGRPC(channel *grpc.ClientConn) ResourceStoreClient {
return NewResourceStoreClient(grpchan.InterceptClientConn(channel, grpcUtils.UnaryClientInterceptor, grpcUtils.StreamClientInterceptor))
}

View File

@ -140,7 +140,7 @@ func NewResourceServer(opts ResourceServerOptions) (ResourceServer, error) {
opts.Blob = &noopService{}
}
if opts.Diagnostics == nil {
opts.Search = &noopService{}
opts.Diagnostics = &noopService{}
}
if opts.Now == nil {
opts.Now = func() int64 {

View File

@ -23,7 +23,7 @@ import (
// Package-level errors.
var (
ErrNotImplementedYet = errors.New("not implemented yet")
ErrNotImplementedYet = errors.New("not implemented yet (sqlnext)")
)
func ProvideSQLResourceServer(db db.EntityDBInterface, tracer tracing.Tracer) (resource.ResourceServer, error) {