diff --git a/pkg/registry/apis/dashboard/access/storage.go b/pkg/registry/apis/dashboard/access/storage.go index cb0934516f0..6dbeca41608 100644 --- a/pkg/registry/apis/dashboard/access/storage.go +++ b/pkg/registry/apis/dashboard/access/storage.go @@ -113,10 +113,9 @@ func (a *dashboardSqlAccess) GetDashboard(ctx context.Context, orgId int64, uid defer func() { _ = rows.Close() }() row, err := rows.Next() - if err != nil { + if err != nil || row == nil { return nil, 0, err } - return row.Dash, row.Version, nil } diff --git a/pkg/registry/apis/dashboard/register.go b/pkg/registry/apis/dashboard/register.go index 9b133a0fdc7..b33f009edf6 100644 --- a/pkg/registry/apis/dashboard/register.go +++ b/pkg/registry/apis/dashboard/register.go @@ -29,6 +29,7 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/provisioning" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/storage/unified/apistore" ) var _ builder.APIGroupBuilder = (*DashboardsAPIBuilder)(nil) @@ -154,9 +155,10 @@ func (b *DashboardsAPIBuilder) GetAPIGroupInfo( storage[dash.StoragePath("dto")] = &DTOConnector{ builder: b, } - storage[dash.StoragePath("versions")] = &VersionsREST{ - search: b.store.server, // resource.NewLocalResourceSearchClient(b.store.server), - } + storage[dash.StoragePath("history")] = apistore.NewHistoryConnector( + b.store.server, // as client??? + dashboard.DashboardResourceInfo.GroupResource(), + ) // // Dual writes if a RESTOptionsGetter is provided // if desiredMode != grafanarest.Mode0 && optsGetter != nil { diff --git a/pkg/registry/apis/dashboard/sub_versions.go b/pkg/registry/apis/dashboard/sub_versions.go deleted file mode 100644 index 000b70bcbdc..00000000000 --- a/pkg/registry/apis/dashboard/sub_versions.go +++ /dev/null @@ -1,125 +0,0 @@ -package dashboard - -import ( - "context" - "encoding/json" - "net/http" - "strconv" - "strings" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apiserver/pkg/registry/rest" - - "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" - "github.com/grafana/grafana/pkg/storage/unified/resource" -) - -type VersionsREST struct { - search resource.ResourceSearchServer // should be a client! -} - -var _ = rest.Connecter(&VersionsREST{}) -var _ = rest.StorageMetadata(&VersionsREST{}) - -func (r *VersionsREST) New() runtime.Object { - return &metav1.PartialObjectMetadataList{} -} - -func (r *VersionsREST) Destroy() { -} - -func (r *VersionsREST) ConnectMethods() []string { - return []string{"GET"} -} - -func (r *VersionsREST) ProducesMIMETypes(verb string) []string { - return nil -} - -func (r *VersionsREST) ProducesObject(verb string) interface{} { - return &metav1.PartialObjectMetadataList{} -} - -func (r *VersionsREST) NewConnectOptions() (runtime.Object, bool, string) { - return nil, true, "" -} - -func (r *VersionsREST) Connect(ctx context.Context, uid string, opts runtime.Object, responder rest.Responder) (http.Handler, error) { - info, err := request.NamespaceInfoFrom(ctx, true) - 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 { - vkey := path[strings.LastIndex(path, "/")+1:] - version, err := strconv.ParseInt(vkey, 10, 64) - if err != nil { - responder.Error(err) - return - } - - dashbytes, err := r.search.Read(ctx, &resource.ReadRequest{ - Key: key, - ResourceVersion: version, - }) - if err != nil { - responder.Error(err) - return - } - - // Convert the version to a regular dashboard - 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 - } - - rsp, err := r.search.History(ctx, &resource.HistoryRequest{ - NextPageToken: "", // TODO! - Limit: 100, - Key: key, - }) - if err != nil { - responder.Error(err) - return - } - - list := &metav1.PartialObjectMetadataList{ - ListMeta: metav1.ListMeta{ - Continue: rsp.NextPageToken, - }, - } - 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/apistore/history.go b/pkg/storage/unified/apistore/history.go new file mode 100644 index 00000000000..ee97760c173 --- /dev/null +++ b/pkg/storage/unified/apistore/history.go @@ -0,0 +1,103 @@ +package apistore + +import ( + "context" + "encoding/json" + "net/http" + "strconv" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/registry/rest" + + "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" + "github.com/grafana/grafana/pkg/storage/unified/resource" +) + +type HistoryConnector interface { + rest.Storage + rest.Connecter + rest.StorageMetadata +} + +func NewHistoryConnector(search resource.ResourceSearchServer, gr schema.GroupResource) HistoryConnector { + return &historyREST{ + search: search, + gr: gr, + } +} + +type historyREST struct { + search resource.ResourceSearchServer // should be a client! + gr schema.GroupResource +} + +func (r *historyREST) New() runtime.Object { + return &metav1.PartialObjectMetadataList{} +} + +func (r *historyREST) Destroy() { +} + +func (r *historyREST) ConnectMethods() []string { + return []string{"GET"} +} + +func (r *historyREST) ProducesMIMETypes(verb string) []string { + return nil +} + +func (r *historyREST) ProducesObject(verb string) interface{} { + return &metav1.PartialObjectMetadataList{} +} + +func (r *historyREST) NewConnectOptions() (runtime.Object, bool, string) { + return nil, false, "" +} + +func (r *historyREST) Connect(ctx context.Context, uid string, opts runtime.Object, responder rest.Responder) (http.Handler, error) { + info, err := request.NamespaceInfoFrom(ctx, true) + if err != nil { + return nil, err + } + + key := &resource.ResourceKey{ + Namespace: info.Value, + Group: r.gr.Group, + Resource: r.gr.Resource, + Name: uid, + } + + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + query := req.URL.Query() + rsp, err := r.search.History(ctx, &resource.HistoryRequest{ + NextPageToken: query.Get("token"), + Limit: 100, // TODO, from query + Key: key, + }) + if err != nil { + responder.Error(err) + return + } + + list := &metav1.PartialObjectMetadataList{ + ListMeta: metav1.ListMeta{ + Continue: rsp.NextPageToken, + }, + } + 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/apistore/storage.go b/pkg/storage/unified/apistore/storage.go index 4bafaba514c..55a8c336ee2 100644 --- a/pkg/storage/unified/apistore/storage.go +++ b/pkg/storage/unified/apistore/storage.go @@ -248,12 +248,21 @@ func (s *Storage) Watch(ctx context.Context, _ string, opts storage.ListOptions) // The returned contents may be delayed, but it is guaranteed that they will // match 'opts.ResourceVersion' according 'opts.ResourceVersionMatch'. func (s *Storage) Get(ctx context.Context, _ string, opts storage.GetOptions, objPtr runtime.Object) error { - key, err := getKey(ctx) + var err error + req := &resource.ReadRequest{} + req.Key, err = getKey(ctx) if err != nil { return err } - rsp, err := s.store.Read(ctx, &resource.ReadRequest{Key: key}) + if opts.ResourceVersion != "" { + req.ResourceVersion, err = strconv.ParseInt(opts.ResourceVersion, 10, 64) + if err != nil { + return err + } + } + + rsp, err := s.store.Read(ctx, req) if err != nil { return err }