mirror of https://github.com/grafana/grafana
Storage: move http wrapper into its own subpackage (#57929)
parent
6d2dced218
commit
6e3a6cce61
@ -1,236 +0,0 @@ |
||||
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/services/store/kind" |
||||
"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 |
||||
kinds kind.KindRegistry |
||||
} |
||||
|
||||
func ProvideHTTPObjectStore(store ObjectStoreServer, kinds kind.KindRegistry) HTTPObjectStore { |
||||
return &httpObjectStore{ |
||||
store: store, |
||||
log: log.New("http-object-store"), |
||||
kinds: kinds, |
||||
} |
||||
} |
||||
|
||||
// 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+1:] |
||||
} else { |
||||
uid = path |
||||
kind = "?" |
||||
} |
||||
|
||||
// Read parameters that are encoded in the URL
|
||||
vals := req.URL.Query() |
||||
for k, v := range vals { |
||||
if len(v) > 0 { |
||||
params[k] = v[0] |
||||
} |
||||
} |
||||
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{ |
||||
GRN: &GRN{ |
||||
UID: uid, |
||||
Kind: kind, |
||||
}, |
||||
Version: params["version"], // ?version = XYZ
|
||||
WithBody: params["body"] != "false", // default to true
|
||||
WithSummary: params["summary"] == "true", // default to 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{ |
||||
GRN: &GRN{ |
||||
UID: uid, |
||||
Kind: kind, |
||||
}, |
||||
Version: params["version"], // ?version = XYZ
|
||||
WithBody: true, |
||||
WithSummary: false, |
||||
}) |
||||
if err != nil { |
||||
return response.Error(500, "?", err) |
||||
} |
||||
info, err := s.kinds.GetInfo(kind) |
||||
if err != nil { |
||||
return response.Error(400, "Unsupported kind", 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
|
||||
) |
||||
} |
||||
mime := info.MimeType |
||||
if mime == "" { |
||||
mime = "application/json" |
||||
} |
||||
return response.CreateNormalResponse( |
||||
http.Header{ |
||||
"Content-Type": []string{mime}, |
||||
"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{ |
||||
GRN: &GRN{ |
||||
UID: uid, |
||||
Kind: kind, |
||||
}, |
||||
Body: b, |
||||
Comment: params["comment"], |
||||
PreviousVersion: params["previousVersion"], |
||||
}) |
||||
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{ |
||||
GRN: &GRN{ |
||||
UID: uid, |
||||
Kind: kind, |
||||
}, |
||||
PreviousVersion: params["previousVersion"], |
||||
}) |
||||
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{ |
||||
GRN: &GRN{ |
||||
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 { |
||||
req := &ObjectSearchRequest{ |
||||
WithBody: true, |
||||
WithLabels: true, |
||||
WithFields: true, |
||||
// TODO!!!
|
||||
} |
||||
|
||||
rsp, err := s.store.Search(c.Req.Context(), req) |
||||
if err != nil { |
||||
return response.Error(500, "?", err) |
||||
} |
||||
return response.JSON(200, rsp) |
||||
} |
||||
@ -0,0 +1,369 @@ |
||||
package httpobjectstore |
||||
|
||||
import ( |
||||
"fmt" |
||||
"io" |
||||
"net/http" |
||||
"net/url" |
||||
"strconv" |
||||
"strings" |
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log" |
||||
"github.com/grafana/grafana/pkg/middleware" |
||||
"github.com/grafana/grafana/pkg/services/store/kind" |
||||
"github.com/grafana/grafana/pkg/services/store/object" |
||||
"github.com/grafana/grafana/pkg/services/store/router" |
||||
"github.com/grafana/grafana/pkg/util" |
||||
"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 object.ObjectStoreServer |
||||
log log.Logger |
||||
kinds kind.KindRegistry |
||||
router router.ObjectStoreRouter |
||||
} |
||||
|
||||
func ProvideHTTPObjectStore(store object.ObjectStoreServer, kinds kind.KindRegistry) HTTPObjectStore { |
||||
return &httpObjectStore{ |
||||
store: store, |
||||
log: log.New("http-object-store"), |
||||
kinds: kinds, |
||||
router: router.NewObjectStoreRouter(kinds), |
||||
} |
||||
} |
||||
|
||||
// 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.Post("/store/*", reqGrafanaAdmin, routing.Wrap(s.doWriteObject)) |
||||
route.Delete("/store/*", reqGrafanaAdmin, routing.Wrap(s.doDeleteObject)) |
||||
route.Get("/raw/*", reqGrafanaAdmin, routing.Wrap(s.doGetRawObject)) |
||||
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)) |
||||
|
||||
// File upload
|
||||
route.Post("/upload/:scope", reqGrafanaAdmin, routing.Wrap(s.doUpload)) |
||||
} |
||||
|
||||
// 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 (s *httpObjectStore) getGRNFromRequest(c *models.ReqContext) (*object.GRN, map[string]string, error) { |
||||
params := web.Params(c.Req) |
||||
info, err := s.router.RouteFromKey(c.Req.Context(), |
||||
fmt.Sprintf("%d/%s", c.OrgID, params["*"])) |
||||
if err != nil { |
||||
return nil, params, err |
||||
} |
||||
|
||||
// Read parameters that are encoded in the URL
|
||||
vals := c.Req.URL.Query() |
||||
for k, v := range vals { |
||||
if len(v) > 0 { |
||||
params[k] = v[0] |
||||
} |
||||
} |
||||
return info.GRN, params, nil |
||||
} |
||||
|
||||
func (s *httpObjectStore) doGetObject(c *models.ReqContext) response.Response { |
||||
grn, params, err := s.getGRNFromRequest(c) |
||||
if err != nil { |
||||
return response.Error(400, err.Error(), err) |
||||
} |
||||
rsp, err := s.store.Read(c.Req.Context(), &object.ReadObjectRequest{ |
||||
GRN: grn, |
||||
Version: params["version"], // ?version = XYZ
|
||||
WithBody: params["body"] != "false", // default to true
|
||||
WithSummary: params["summary"] == "true", // default to 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 { |
||||
grn, params, err := s.getGRNFromRequest(c) |
||||
if err != nil { |
||||
return response.Error(400, err.Error(), err) |
||||
} |
||||
rsp, err := s.store.Read(c.Req.Context(), &object.ReadObjectRequest{ |
||||
GRN: grn, |
||||
Version: params["version"], // ?version = XYZ
|
||||
WithBody: true, |
||||
WithSummary: false, |
||||
}) |
||||
if err != nil { |
||||
return response.Error(500, "?", err) |
||||
} |
||||
info, err := s.kinds.GetInfo(grn.Kind) |
||||
if err != nil { |
||||
return response.Error(400, "Unsupported kind", 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
|
||||
) |
||||
} |
||||
mime := info.MimeType |
||||
if mime == "" { |
||||
mime = "application/json" |
||||
} |
||||
return response.CreateNormalResponse( |
||||
http.Header{ |
||||
"Content-Type": []string{mime}, |
||||
"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 { |
||||
grn, params, err := s.getGRNFromRequest(c) |
||||
if err != nil { |
||||
return response.Error(400, err.Error(), err) |
||||
} |
||||
|
||||
// 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(), &object.WriteObjectRequest{ |
||||
GRN: grn, |
||||
Body: b, |
||||
Comment: params["comment"], |
||||
PreviousVersion: params["previousVersion"], |
||||
}) |
||||
if err != nil { |
||||
return response.Error(500, "?", err) |
||||
} |
||||
return response.JSON(200, rsp) |
||||
} |
||||
|
||||
func (s *httpObjectStore) doDeleteObject(c *models.ReqContext) response.Response { |
||||
grn, params, err := s.getGRNFromRequest(c) |
||||
if err != nil { |
||||
return response.Error(400, err.Error(), err) |
||||
} |
||||
rsp, err := s.store.Delete(c.Req.Context(), &object.DeleteObjectRequest{ |
||||
GRN: grn, |
||||
PreviousVersion: params["previousVersion"], |
||||
}) |
||||
if err != nil { |
||||
return response.Error(500, "?", err) |
||||
} |
||||
return response.JSON(200, rsp) |
||||
} |
||||
|
||||
func (s *httpObjectStore) doGetHistory(c *models.ReqContext) response.Response { |
||||
grn, params, err := s.getGRNFromRequest(c) |
||||
if err != nil { |
||||
return response.Error(400, err.Error(), err) |
||||
} |
||||
limit := int64(20) // params
|
||||
rsp, err := s.store.History(c.Req.Context(), &object.ObjectHistoryRequest{ |
||||
GRN: grn, |
||||
Limit: limit, |
||||
NextPageToken: params["nextPageToken"], |
||||
}) |
||||
if err != nil { |
||||
return response.Error(500, "?", err) |
||||
} |
||||
return response.JSON(200, rsp) |
||||
} |
||||
|
||||
func (s *httpObjectStore) doUpload(c *models.ReqContext) response.Response { |
||||
c.Req.Body = http.MaxBytesReader(c.Resp, c.Req.Body, MAX_UPLOAD_SIZE) |
||||
if err := c.Req.ParseMultipartForm(MAX_UPLOAD_SIZE); err != nil { |
||||
msg := fmt.Sprintf("Please limit file uploaded under %s", util.ByteCountSI(MAX_UPLOAD_SIZE)) |
||||
return response.Error(400, msg, nil) |
||||
} |
||||
fileinfo := c.Req.MultipartForm.File |
||||
if len(fileinfo) < 1 { |
||||
return response.Error(400, "missing files", nil) |
||||
} |
||||
scope := web.Params(c.Req)[":scope"] |
||||
if scope == "" { |
||||
return response.Error(400, "invalid scope", nil) |
||||
} |
||||
|
||||
var rsp []*object.WriteObjectResponse |
||||
|
||||
message := getMultipartFormValue(c.Req, "message") |
||||
overwriteExistingFile := getMultipartFormValue(c.Req, "overwriteExistingFile") != "false" // must explicitly overwrite
|
||||
folder := getMultipartFormValue(c.Req, "folder") |
||||
ctx := c.Req.Context() |
||||
|
||||
for _, fileHeaders := range fileinfo { |
||||
for _, fileHeader := range fileHeaders { |
||||
idx := strings.LastIndex(fileHeader.Filename, ".") |
||||
if idx <= 0 { |
||||
return response.Error(400, "Expecting file extension: "+fileHeader.Filename, nil) |
||||
} |
||||
|
||||
ext := strings.ToLower(fileHeader.Filename[idx+1:]) |
||||
kind, err := s.kinds.GetFromExtension(ext) |
||||
if err != nil || kind.ID == "" { |
||||
return response.Error(400, "Unsupported kind: "+fileHeader.Filename, err) |
||||
} |
||||
uid := folder |
||||
if uid != "" { |
||||
uid = uid + "/" + fileHeader.Filename[:idx] |
||||
} else { |
||||
uid = fileHeader.Filename[:idx] |
||||
} |
||||
|
||||
file, err := fileHeader.Open() |
||||
if err != nil { |
||||
return response.Error(500, "Internal Server Error", err) |
||||
} |
||||
data, err := io.ReadAll(file) |
||||
if err != nil { |
||||
return response.Error(500, "Internal Server Error", err) |
||||
} |
||||
err = file.Close() |
||||
if err != nil { |
||||
return response.Error(500, "Internal Server Error", err) |
||||
} |
||||
|
||||
grn := &object.GRN{ |
||||
Scope: scope, |
||||
UID: uid, |
||||
Kind: kind.ID, |
||||
} |
||||
|
||||
if !overwriteExistingFile { |
||||
result, err := s.store.Read(ctx, &object.ReadObjectRequest{ |
||||
GRN: grn, |
||||
WithBody: false, |
||||
WithSummary: false, |
||||
}) |
||||
if err != nil { |
||||
return response.Error(500, "Internal Server Error", err) |
||||
} |
||||
if result.Object != nil { |
||||
return response.Error(400, "File name already in use", err) |
||||
} |
||||
} |
||||
|
||||
result, err := s.store.Write(ctx, &object.WriteObjectRequest{ |
||||
GRN: grn, |
||||
Body: data, |
||||
Comment: message, |
||||
// PreviousVersion: params["previousVersion"],
|
||||
}) |
||||
|
||||
if err != nil { |
||||
return response.Error(500, err.Error(), err) // TODO, better errors
|
||||
} |
||||
rsp = append(rsp, result) |
||||
} |
||||
} |
||||
|
||||
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 { |
||||
vals := c.Req.URL.Query() |
||||
|
||||
req := &object.ObjectSearchRequest{ |
||||
WithBody: asBoolean("body", vals, false), |
||||
WithLabels: asBoolean("labels", vals, true), |
||||
WithFields: asBoolean("fields", vals, true), |
||||
Kind: vals["kind"], |
||||
Query: vals.Get("query"), |
||||
Folder: vals.Get("folder"), |
||||
Sort: vals["sort"], |
||||
} |
||||
if vals.Has("limit") { |
||||
limit, err := strconv.ParseInt(vals.Get("limit"), 10, 64) |
||||
if err != nil { |
||||
return response.Error(400, "bad limit", err) |
||||
} |
||||
req.Limit = limit |
||||
} |
||||
|
||||
rsp, err := s.store.Search(c.Req.Context(), req) |
||||
if err != nil { |
||||
return response.Error(500, "?", err) |
||||
} |
||||
return response.JSON(200, rsp) |
||||
} |
||||
|
||||
func asBoolean(key string, vals url.Values, defaultValue bool) bool { |
||||
v, ok := vals[key] |
||||
if !ok { |
||||
return defaultValue |
||||
} |
||||
if len(v) == 0 { |
||||
return true // single boolean parameter
|
||||
} |
||||
b, err := strconv.ParseBool(v[0]) |
||||
if err != nil { |
||||
return defaultValue |
||||
} |
||||
return b |
||||
} |
||||
|
||||
func getMultipartFormValue(req *http.Request, key string) string { |
||||
v, ok := req.MultipartForm.Value[key] |
||||
if !ok || len(v) != 1 { |
||||
return "" |
||||
} |
||||
return v[0] |
||||
} |
||||
Loading…
Reference in new issue