The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/services/store/object/http.go

204 lines
5.8 KiB

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 = "?"
}
// 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{
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["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{
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{
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")
}