diff --git a/pkg/registry/apis/dashboard/access/sql_dashboards.go b/pkg/registry/apis/dashboard/access/sql_dashboards.go index 2c5e9ae2422..efddb3d7497 100644 --- a/pkg/registry/apis/dashboard/access/sql_dashboards.go +++ b/pkg/registry/apis/dashboard/access/sql_dashboards.go @@ -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, } } diff --git a/pkg/registry/apis/dashboard/access/storage.go b/pkg/registry/apis/dashboard/access/storage.go index de033a51015..ea5501a073f 100644 --- a/pkg/registry/apis/dashboard/access/storage.go +++ b/pkg/registry/apis/dashboard/access/storage.go @@ -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)") +} diff --git a/pkg/registry/apis/dashboard/access/types.go b/pkg/registry/apis/dashboard/access/types.go index d6bb2f14c87..f9d7cf67711 100644 --- a/pkg/registry/apis/dashboard/access/types.go +++ b/pkg/registry/apis/dashboard/access/types.go @@ -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) diff --git a/pkg/registry/apis/dashboard/legacy_storage.go b/pkg/registry/apis/dashboard/legacy_storage.go index a86de3bf740..6e6605692ca 100644 --- a/pkg/registry/apis/dashboard/legacy_storage.go +++ b/pkg/registry/apis/dashboard/legacy_storage.go @@ -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()) diff --git a/pkg/registry/apis/dashboard/register.go b/pkg/registry/apis/dashboard/register.go index 0a9571d1b41..7f2808bd29b 100644 --- a/pkg/registry/apis/dashboard/register.go +++ b/pkg/registry/apis/dashboard/register.go @@ -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 diff --git a/pkg/registry/apis/dashboard/sub_versions.go b/pkg/registry/apis/dashboard/sub_versions.go index 4787c5387df..000b70bcbdc 100644 --- a/pkg/registry/apis/dashboard/sub_versions.go +++ b/pkg/registry/apis/dashboard/sub_versions.go @@ -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 } diff --git a/pkg/storage/unified/resource/client_wrapper.go b/pkg/storage/unified/resource/client_wrapper.go index 7ad67e937d8..ea71dee90a4 100644 --- a/pkg/storage/unified/resource/client_wrapper.go +++ b/pkg/storage/unified/resource/client_wrapper.go @@ -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)) } diff --git a/pkg/storage/unified/resource/server.go b/pkg/storage/unified/resource/server.go index f9276e70714..c57d4cbcab1 100644 --- a/pkg/storage/unified/resource/server.go +++ b/pkg/storage/unified/resource/server.go @@ -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 { diff --git a/pkg/storage/unified/sqlnext/sql_resources.go b/pkg/storage/unified/sqlnext/sql_resources.go index 9d525a700df..e2320bb67e6 100644 --- a/pkg/storage/unified/sqlnext/sql_resources.go +++ b/pkg/storage/unified/sqlnext/sql_resources.go @@ -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) {