mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Storage: Add HTTP endpoint for object store service (#56214)
This commit is contained in:
parent
bba6eb1f2d
commit
d5e2713168
@ -261,7 +261,13 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if hs.Features.IsEnabled(featuremgmt.FlagStorage) {
|
if hs.Features.IsEnabled(featuremgmt.FlagStorage) {
|
||||||
|
// Will eventually be replaced with the 'object' route
|
||||||
apiRoute.Group("/storage", hs.StorageService.RegisterHTTPRoutes)
|
apiRoute.Group("/storage", hs.StorageService.RegisterHTTPRoutes)
|
||||||
|
|
||||||
|
// Allow HTTP access to the object storage feature (dev only for now)
|
||||||
|
if hs.Features.IsEnabled(featuremgmt.FlagGrpcServer) {
|
||||||
|
apiRoute.Group("/object", hs.httpObjectStore.RegisterHTTPRoutes)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if hs.Features.IsEnabled(featuremgmt.FlagPanelTitleSearch) {
|
if hs.Features.IsEnabled(featuremgmt.FlagPanelTitleSearch) {
|
||||||
|
@ -16,6 +16,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
"github.com/grafana/grafana/pkg/middleware/csrf"
|
"github.com/grafana/grafana/pkg/middleware/csrf"
|
||||||
"github.com/grafana/grafana/pkg/services/searchV2"
|
"github.com/grafana/grafana/pkg/services/searchV2"
|
||||||
|
"github.com/grafana/grafana/pkg/services/store/object"
|
||||||
"github.com/grafana/grafana/pkg/services/userauth"
|
"github.com/grafana/grafana/pkg/services/userauth"
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
@ -140,6 +141,7 @@ type HTTPServer struct {
|
|||||||
ThumbService thumbs.Service
|
ThumbService thumbs.Service
|
||||||
ExportService export.ExportService
|
ExportService export.ExportService
|
||||||
StorageService store.StorageService
|
StorageService store.StorageService
|
||||||
|
httpObjectStore object.HTTPObjectStore
|
||||||
SearchV2HTTPService searchV2.SearchHTTPService
|
SearchV2HTTPService searchV2.SearchHTTPService
|
||||||
ContextHandler *contexthandler.ContextHandler
|
ContextHandler *contexthandler.ContextHandler
|
||||||
SQLStore sqlstore.Store
|
SQLStore sqlstore.Store
|
||||||
@ -225,7 +227,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
|
|||||||
pluginsUpdateChecker *updatechecker.PluginsService, searchUsersService searchusers.Service,
|
pluginsUpdateChecker *updatechecker.PluginsService, searchUsersService searchusers.Service,
|
||||||
dataSourcesService datasources.DataSourceService, queryDataService *query.Service,
|
dataSourcesService datasources.DataSourceService, queryDataService *query.Service,
|
||||||
ldapGroups ldap.Groups, teamGuardian teamguardian.TeamGuardian, serviceaccountsService serviceaccounts.Service,
|
ldapGroups ldap.Groups, teamGuardian teamguardian.TeamGuardian, serviceaccountsService serviceaccounts.Service,
|
||||||
authInfoService login.AuthInfoService, storageService store.StorageService,
|
authInfoService login.AuthInfoService, storageService store.StorageService, httpObjectStore object.HTTPObjectStore,
|
||||||
notificationService *notifications.NotificationService, dashboardService dashboards.DashboardService,
|
notificationService *notifications.NotificationService, dashboardService dashboards.DashboardService,
|
||||||
dashboardProvisioningService dashboards.DashboardProvisioningService, folderService dashboards.FolderService,
|
dashboardProvisioningService dashboards.DashboardProvisioningService, folderService dashboards.FolderService,
|
||||||
datasourcePermissionsService permissions.DatasourcePermissionsService, alertNotificationService *alerting.AlertNotificationService,
|
datasourcePermissionsService permissions.DatasourcePermissionsService, alertNotificationService *alerting.AlertNotificationService,
|
||||||
@ -302,6 +304,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
|
|||||||
secretsMigrator: secretsMigrator,
|
secretsMigrator: secretsMigrator,
|
||||||
secretsPluginMigrator: secretsPluginMigrator,
|
secretsPluginMigrator: secretsPluginMigrator,
|
||||||
secretsStore: secretsStore,
|
secretsStore: secretsStore,
|
||||||
|
httpObjectStore: httpObjectStore,
|
||||||
DataSourcesService: dataSourcesService,
|
DataSourcesService: dataSourcesService,
|
||||||
searchUsersService: searchUsersService,
|
searchUsersService: searchUsersService,
|
||||||
ldapGroups: ldapGroups,
|
ldapGroups: ldapGroups,
|
||||||
|
@ -121,6 +121,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
|
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
|
||||||
"github.com/grafana/grafana/pkg/services/star/starimpl"
|
"github.com/grafana/grafana/pkg/services/star/starimpl"
|
||||||
"github.com/grafana/grafana/pkg/services/store"
|
"github.com/grafana/grafana/pkg/services/store"
|
||||||
|
"github.com/grafana/grafana/pkg/services/store/object"
|
||||||
objectdummyserver "github.com/grafana/grafana/pkg/services/store/object/dummy"
|
objectdummyserver "github.com/grafana/grafana/pkg/services/store/object/dummy"
|
||||||
"github.com/grafana/grafana/pkg/services/store/sanitizer"
|
"github.com/grafana/grafana/pkg/services/store/sanitizer"
|
||||||
"github.com/grafana/grafana/pkg/services/tag"
|
"github.com/grafana/grafana/pkg/services/tag"
|
||||||
@ -347,6 +348,7 @@ var wireBasicSet = wire.NewSet(
|
|||||||
grpcserver.ProvideHealthService,
|
grpcserver.ProvideHealthService,
|
||||||
grpcserver.ProvideReflectionService,
|
grpcserver.ProvideReflectionService,
|
||||||
objectdummyserver.ProvideDummyObjectServer,
|
objectdummyserver.ProvideDummyObjectServer,
|
||||||
|
object.ProvideHTTPObjectStore,
|
||||||
teamimpl.ProvideService,
|
teamimpl.ProvideService,
|
||||||
tempuserimpl.ProvideService,
|
tempuserimpl.ProvideService,
|
||||||
loginattemptimpl.ProvideService,
|
loginattemptimpl.ProvideService,
|
||||||
|
196
pkg/services/store/object/http.go
Normal file
196
pkg/services/store/object/http.go
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
package object
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
|
"github.com/grafana/grafana/pkg/middleware"
|
||||||
|
"github.com/grafana/grafana/pkg/web"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/api/response"
|
||||||
|
"github.com/grafana/grafana/pkg/api/routing"
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HTTPObjectStore interface {
|
||||||
|
// Register HTTP Access to the store
|
||||||
|
RegisterHTTPRoutes(routing.RouteRegister)
|
||||||
|
}
|
||||||
|
|
||||||
|
type httpObjectStore struct {
|
||||||
|
store ObjectStoreServer
|
||||||
|
log log.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func ProvideHTTPObjectStore(store ObjectStoreServer) HTTPObjectStore {
|
||||||
|
return &httpObjectStore{
|
||||||
|
store: store,
|
||||||
|
log: log.New("http-object-store"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All registered under "api/object"
|
||||||
|
func (s *httpObjectStore) RegisterHTTPRoutes(route routing.RouteRegister) {
|
||||||
|
// For now, require admin for everything
|
||||||
|
reqGrafanaAdmin := middleware.ReqSignedIn //.ReqGrafanaAdmin
|
||||||
|
|
||||||
|
// Every * must parse to a GRN (uid+kind)
|
||||||
|
route.Get("/store/*", reqGrafanaAdmin, routing.Wrap(s.doGetObject))
|
||||||
|
route.Get("/raw/*", reqGrafanaAdmin, routing.Wrap(s.doGetRawObject))
|
||||||
|
route.Post("/store/*", reqGrafanaAdmin, routing.Wrap(s.doWriteObject))
|
||||||
|
route.Delete("/store/*", reqGrafanaAdmin, routing.Wrap(s.doDeleteObject))
|
||||||
|
route.Get("/history/*", reqGrafanaAdmin, routing.Wrap(s.doGetHistory))
|
||||||
|
route.Get("/list/*", reqGrafanaAdmin, routing.Wrap(s.doListFolder)) // Simplified version of search -- path is prefix
|
||||||
|
route.Get("/search", reqGrafanaAdmin, routing.Wrap(s.doSearch))
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function will extract UID+Kind from the requested path "*" in our router
|
||||||
|
// This is far from ideal! but is at least consistent for these endpoints.
|
||||||
|
// This will quickly be revisited as we explore how to encode UID+Kind in a "GRN" format
|
||||||
|
func parseRequestParams(req *http.Request) (uid string, kind string, params map[string]string) {
|
||||||
|
params = web.Params(req)
|
||||||
|
path := params["*"]
|
||||||
|
idx := strings.LastIndex(path, ".")
|
||||||
|
if idx > 0 {
|
||||||
|
uid = path[:idx]
|
||||||
|
kind = path[idx:]
|
||||||
|
} else {
|
||||||
|
uid = path
|
||||||
|
kind = "?"
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *httpObjectStore) doGetObject(c *models.ReqContext) response.Response {
|
||||||
|
uid, kind, params := parseRequestParams(c.Req)
|
||||||
|
rsp, err := s.store.Read(c.Req.Context(), &ReadObjectRequest{
|
||||||
|
UID: uid,
|
||||||
|
Kind: kind,
|
||||||
|
Version: params["version"], // ?version = XYZ
|
||||||
|
WithBody: true, // ?? allow false?
|
||||||
|
WithSummary: true, // ?? allow false?
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return response.Error(500, "error fetching object", err)
|
||||||
|
}
|
||||||
|
if rsp.Object == nil {
|
||||||
|
return response.Error(404, "not found", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure etag support
|
||||||
|
currentEtag := rsp.Object.ETag
|
||||||
|
previousEtag := c.Req.Header.Get("If-None-Match")
|
||||||
|
if previousEtag == currentEtag {
|
||||||
|
return response.CreateNormalResponse(
|
||||||
|
http.Header{
|
||||||
|
"ETag": []string{rsp.Object.ETag},
|
||||||
|
},
|
||||||
|
[]byte{}, // nothing
|
||||||
|
http.StatusNotModified, // 304
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Resp.Header().Set("ETag", currentEtag)
|
||||||
|
return response.JSON(200, rsp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *httpObjectStore) doGetRawObject(c *models.ReqContext) response.Response {
|
||||||
|
uid, kind, params := parseRequestParams(c.Req)
|
||||||
|
rsp, err := s.store.Read(c.Req.Context(), &ReadObjectRequest{
|
||||||
|
UID: uid,
|
||||||
|
Kind: kind,
|
||||||
|
Version: params["version"], // ?version = XYZ
|
||||||
|
WithBody: true,
|
||||||
|
WithSummary: false,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return response.Error(500, "?", err)
|
||||||
|
}
|
||||||
|
if rsp.Object != nil && rsp.Object.Body != nil {
|
||||||
|
// Configure etag support
|
||||||
|
currentEtag := rsp.Object.ETag
|
||||||
|
previousEtag := c.Req.Header.Get("If-None-Match")
|
||||||
|
if previousEtag == currentEtag {
|
||||||
|
return response.CreateNormalResponse(
|
||||||
|
http.Header{
|
||||||
|
"ETag": []string{rsp.Object.ETag},
|
||||||
|
},
|
||||||
|
[]byte{}, // nothing
|
||||||
|
http.StatusNotModified, // 304
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.CreateNormalResponse(
|
||||||
|
http.Header{
|
||||||
|
"Content-Type": []string{"application/json"}, // TODO, based on kind!!!
|
||||||
|
"ETag": []string{currentEtag},
|
||||||
|
},
|
||||||
|
rsp.Object.Body,
|
||||||
|
200,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return response.JSON(400, rsp) // ???
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_UPLOAD_SIZE = 5 * 1024 * 1024 // 5MB
|
||||||
|
|
||||||
|
func (s *httpObjectStore) doWriteObject(c *models.ReqContext) response.Response {
|
||||||
|
uid, kind, params := parseRequestParams(c.Req)
|
||||||
|
|
||||||
|
// Cap the max size
|
||||||
|
c.Req.Body = http.MaxBytesReader(c.Resp, c.Req.Body, MAX_UPLOAD_SIZE)
|
||||||
|
b, err := io.ReadAll(c.Req.Body)
|
||||||
|
if err != nil {
|
||||||
|
return response.Error(400, "error reading body", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rsp, err := s.store.Write(c.Req.Context(), &WriteObjectRequest{
|
||||||
|
UID: uid,
|
||||||
|
Kind: kind,
|
||||||
|
Body: b,
|
||||||
|
Comment: params["comment"],
|
||||||
|
PreviousVersion: params["previous"],
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return response.Error(500, "?", err)
|
||||||
|
}
|
||||||
|
return response.JSON(200, rsp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *httpObjectStore) doDeleteObject(c *models.ReqContext) response.Response {
|
||||||
|
uid, kind, params := parseRequestParams(c.Req)
|
||||||
|
rsp, err := s.store.Delete(c.Req.Context(), &DeleteObjectRequest{
|
||||||
|
UID: uid,
|
||||||
|
Kind: kind,
|
||||||
|
PreviousVersion: params["previous"],
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return response.Error(500, "?", err)
|
||||||
|
}
|
||||||
|
return response.JSON(200, rsp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *httpObjectStore) doGetHistory(c *models.ReqContext) response.Response {
|
||||||
|
uid, kind, params := parseRequestParams(c.Req)
|
||||||
|
limit := int64(20) // params
|
||||||
|
rsp, err := s.store.History(c.Req.Context(), &ObjectHistoryRequest{
|
||||||
|
UID: uid,
|
||||||
|
Kind: kind,
|
||||||
|
Limit: limit,
|
||||||
|
NextPageToken: params["nextPageToken"],
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return response.Error(500, "?", err)
|
||||||
|
}
|
||||||
|
return response.JSON(200, rsp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *httpObjectStore) doListFolder(c *models.ReqContext) response.Response {
|
||||||
|
return response.JSON(501, "Not implemented yet")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *httpObjectStore) doSearch(c *models.ReqContext) response.Response {
|
||||||
|
return response.JSON(501, "Not implemented yet")
|
||||||
|
}
|
101
pkg/services/store/object/json.go
Normal file
101
pkg/services/store/object/json.go
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
package object
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
jsoniter "github.com/json-iterator/go"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() { //nolint:gochecknoinits
|
||||||
|
//jsoniter.RegisterTypeEncoder("object.ReadObjectResponse", &readObjectResponseCodec{})
|
||||||
|
jsoniter.RegisterTypeEncoder("object.RawObject", &rawObjectCodec{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unlike the standard JSON marshal, this will write bytes as JSON when it can
|
||||||
|
type rawObjectCodec struct{}
|
||||||
|
|
||||||
|
// Custom marshal for RawObject (if JSON body)
|
||||||
|
func (obj *RawObject) MarshalJSON() ([]byte, error) {
|
||||||
|
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||||
|
return json.Marshal(obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (codec *rawObjectCodec) IsEmpty(ptr unsafe.Pointer) bool {
|
||||||
|
f := (*RawObject)(ptr)
|
||||||
|
return f.UID == "" && f.Body == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (codec *rawObjectCodec) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) {
|
||||||
|
obj := (*RawObject)(ptr)
|
||||||
|
stream.WriteObjectStart()
|
||||||
|
stream.WriteObjectField("UID")
|
||||||
|
stream.WriteString(obj.UID)
|
||||||
|
|
||||||
|
if obj.Kind != "" {
|
||||||
|
stream.WriteMore()
|
||||||
|
stream.WriteObjectField("kind")
|
||||||
|
stream.WriteString(obj.Kind)
|
||||||
|
}
|
||||||
|
if obj.Created > 0 {
|
||||||
|
stream.WriteMore()
|
||||||
|
stream.WriteObjectField("created")
|
||||||
|
stream.WriteInt64(obj.Created)
|
||||||
|
}
|
||||||
|
if obj.CreatedBy != nil {
|
||||||
|
stream.WriteMore()
|
||||||
|
stream.WriteObjectField("createdBy")
|
||||||
|
stream.WriteVal(obj.CreatedBy)
|
||||||
|
}
|
||||||
|
if obj.Modified > 0 {
|
||||||
|
stream.WriteMore()
|
||||||
|
stream.WriteObjectField("modified")
|
||||||
|
stream.WriteInt64(obj.Modified)
|
||||||
|
}
|
||||||
|
if obj.ModifiedBy != nil {
|
||||||
|
stream.WriteMore()
|
||||||
|
stream.WriteObjectField("modifiedBy")
|
||||||
|
stream.WriteVal(obj.ModifiedBy)
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj.Size > 0 {
|
||||||
|
stream.WriteMore()
|
||||||
|
stream.WriteObjectField("size")
|
||||||
|
stream.WriteInt64(obj.Size)
|
||||||
|
}
|
||||||
|
if obj.ETag != "" {
|
||||||
|
stream.WriteMore()
|
||||||
|
stream.WriteObjectField("etag")
|
||||||
|
stream.WriteString(obj.ETag)
|
||||||
|
}
|
||||||
|
if obj.Version != "" {
|
||||||
|
stream.WriteMore()
|
||||||
|
stream.WriteObjectField("version")
|
||||||
|
stream.WriteString(obj.Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The one real difference (encodes JSON things directly)
|
||||||
|
if obj.Body != nil {
|
||||||
|
stream.WriteMore()
|
||||||
|
stream.WriteObjectField("body")
|
||||||
|
if json.Valid(obj.Body) {
|
||||||
|
stream.WriteRaw(string(obj.Body)) // works for strings
|
||||||
|
} else {
|
||||||
|
stream.WriteString("// link to raw bytes //")
|
||||||
|
//stream.WriteVal(obj.Body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj.SyncSrc != "" {
|
||||||
|
stream.WriteMore()
|
||||||
|
stream.WriteObjectField("syncSrc")
|
||||||
|
stream.WriteString(obj.SyncSrc)
|
||||||
|
}
|
||||||
|
if obj.SyncTime > 0 {
|
||||||
|
stream.WriteMore()
|
||||||
|
stream.WriteObjectField("syncTime")
|
||||||
|
stream.WriteInt64(obj.SyncTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.WriteObjectEnd()
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user