EntityStore: Remove http access (can use apiserver now) (#77602)

This commit is contained in:
Ryan McKinley 2023-11-03 08:14:51 -07:00 committed by GitHub
parent dd654fdc87
commit 35c1ee9686
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 1 additions and 368 deletions

View File

@ -282,11 +282,6 @@ func (hs *HTTPServer) registerRoutes() {
apiRoute.Group("/storage", hs.StorageService.RegisterHTTPRoutes)
}
// Allow HTTP access to the entity storage feature (dev only for now)
if hs.Features.IsEnabled(featuremgmt.FlagEntityStore) {
apiRoute.Group("/entity", hs.httpEntityStore.RegisterHTTPRoutes)
}
if hs.Features.IsEnabled(featuremgmt.FlagPanelTitleSearch) {
apiRoute.Group("/search-v2", hs.SearchV2HTTPService.RegisterHTTPRoutes)
}

View File

@ -93,7 +93,6 @@ import (
starApi "github.com/grafana/grafana/pkg/services/star/api"
"github.com/grafana/grafana/pkg/services/stats"
"github.com/grafana/grafana/pkg/services/store"
"github.com/grafana/grafana/pkg/services/store/entity/httpentitystore"
"github.com/grafana/grafana/pkg/services/tag"
"github.com/grafana/grafana/pkg/services/team"
tempUser "github.com/grafana/grafana/pkg/services/temp_user"
@ -146,7 +145,6 @@ type HTTPServer struct {
Live *live.GrafanaLive
LivePushGateway *pushhttp.Gateway
StorageService store.StorageService
httpEntityStore httpentitystore.HTTPEntityStore
SearchV2HTTPService searchV2.SearchHTTPService
ContextHandler *contexthandler.ContextHandler
LoggerMiddleware loggermw.Logger
@ -232,7 +230,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
pluginsUpdateChecker *updatechecker.PluginsService, searchUsersService searchusers.Service,
dataSourcesService datasources.DataSourceService, queryDataService query.Service, pluginFileStore plugins.FileStore,
serviceaccountsService serviceaccounts.Service,
authInfoService login.AuthInfoService, storageService store.StorageService, httpEntityStore httpentitystore.HTTPEntityStore,
authInfoService login.AuthInfoService, storageService store.StorageService,
notificationService *notifications.NotificationService, dashboardService dashboards.DashboardService,
dashboardProvisioningService dashboards.DashboardProvisioningService, folderService folder.Service,
dsGuardian guardian.DatasourceGuardianProvider, alertNotificationService *alerting.AlertNotificationService,
@ -309,7 +307,6 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
secretsMigrator: secretsMigrator,
secretsPluginMigrator: secretsPluginMigrator,
secretsStore: secretsStore,
httpEntityStore: httpEntityStore,
DataSourcesService: dataSourcesService,
searchUsersService: searchUsersService,
queryDataService: queryDataService,

View File

@ -134,7 +134,6 @@ import (
"github.com/grafana/grafana/pkg/services/star/starimpl"
"github.com/grafana/grafana/pkg/services/stats/statsimpl"
"github.com/grafana/grafana/pkg/services/store"
"github.com/grafana/grafana/pkg/services/store/entity/httpentitystore"
"github.com/grafana/grafana/pkg/services/store/entity/sqlstash"
"github.com/grafana/grafana/pkg/services/store/kind"
"github.com/grafana/grafana/pkg/services/store/resolver"
@ -347,7 +346,6 @@ var wireBasicSet = wire.NewSet(
kind.ProvideService, // The registry of known kinds
sqlstash.ProvideSQLEntityServer,
resolver.ProvideEntityReferenceResolver,
httpentitystore.ProvideHTTPEntityStore,
teamimpl.ProvideService,
teamapi.ProvideTeamAPI,
tempuserimpl.ProvideService,

View File

@ -1,357 +0,0 @@
package httpentitystore
import (
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/infra/grn"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/middleware"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/store/entity"
"github.com/grafana/grafana/pkg/services/store/kind"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web"
)
type HTTPEntityStore interface {
// Register HTTP Access to the store
RegisterHTTPRoutes(routing.RouteRegister)
}
type httpEntityStore struct {
store entity.EntityStoreServer
log log.Logger
kinds kind.KindRegistry
}
func ProvideHTTPEntityStore(store entity.EntityStoreServer, kinds kind.KindRegistry) HTTPEntityStore {
return &httpEntityStore{
store: store,
log: log.New("http-entity-store"),
kinds: kinds,
}
}
// All registered under "api/entity"
func (s *httpEntityStore) 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/:kind/:uid", reqGrafanaAdmin, routing.Wrap(s.doGetEntity))
route.Post("/store/:kind/:uid", reqGrafanaAdmin, routing.Wrap(s.doWriteEntity))
route.Delete("/store/:kind/:uid", reqGrafanaAdmin, routing.Wrap(s.doDeleteEntity))
route.Get("/raw/:kind/:uid", reqGrafanaAdmin, routing.Wrap(s.doGetRawEntity))
route.Get("/history/:kind/:uid", reqGrafanaAdmin, routing.Wrap(s.doGetHistory))
route.Get("/list/:uid", 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", 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 *httpEntityStore) getGRNFromRequest(c *contextmodel.ReqContext) (*grn.GRN, map[string]string, error) {
params := web.Params(c.Req)
// 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 &grn.GRN{
TenantID: c.SignedInUser.GetOrgID(),
ResourceKind: params[":kind"],
ResourceIdentifier: params[":uid"],
}, params, nil
}
func (s *httpEntityStore) doGetEntity(c *contextmodel.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(), &entity.ReadEntityRequest{
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 entity", err)
}
if rsp == nil {
return response.Error(404, "not found", nil)
}
// Configure etag support
currentEtag := rsp.ETag
previousEtag := c.Req.Header.Get("If-None-Match")
if previousEtag == currentEtag {
return response.CreateNormalResponse(
http.Header{
"ETag": []string{rsp.ETag},
},
[]byte{}, // nothing
http.StatusNotModified, // 304
)
}
c.Resp.Header().Set("ETag", currentEtag)
return response.JSON(200, rsp)
}
func (s *httpEntityStore) doGetRawEntity(c *contextmodel.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(), &entity.ReadEntityRequest{
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.ResourceKind)
if err != nil {
return response.Error(400, "Unsupported kind", err)
}
if rsp != nil && rsp.Body != nil {
// Configure etag support
currentEtag := rsp.ETag
previousEtag := c.Req.Header.Get("If-None-Match")
if previousEtag == currentEtag {
return response.CreateNormalResponse(
http.Header{
"ETag": []string{rsp.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.Body,
200,
)
}
return response.JSON(400, rsp) // ???
}
const MAX_UPLOAD_SIZE = 5 * 1024 * 1024 // 5MB
func (s *httpEntityStore) doWriteEntity(c *contextmodel.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(), &entity.WriteEntityRequest{
GRN: grn,
Body: b,
Folder: params["folder"],
Comment: params["comment"],
PreviousVersion: params["previousVersion"],
})
if err != nil {
return response.Error(500, "?", err)
}
return response.JSON(200, rsp)
}
func (s *httpEntityStore) doDeleteEntity(c *contextmodel.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(), &entity.DeleteEntityRequest{
GRN: grn,
PreviousVersion: params["previousVersion"],
})
if err != nil {
return response.Error(500, "?", err)
}
return response.JSON(200, rsp)
}
func (s *httpEntityStore) doGetHistory(c *contextmodel.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(), &entity.EntityHistoryRequest{
GRN: grn,
Limit: limit,
NextPageToken: params["nextPageToken"],
})
if err != nil {
return response.Error(500, "?", err)
}
return response.JSON(200, rsp)
}
func (s *httpEntityStore) doUpload(c *contextmodel.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)
}
var rsp []*entity.WriteEntityResponse
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 := 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 := &grn.GRN{
ResourceIdentifier: uid,
ResourceKind: kind.ID,
TenantID: c.SignedInUser.GetOrgID(),
}
if !overwriteExistingFile {
result, err := s.store.Read(ctx, &entity.ReadEntityRequest{
GRN: grn,
WithBody: false,
WithSummary: false,
})
if err != nil {
return response.Error(500, "Internal Server Error", err)
}
if result.GRN != nil {
return response.Error(400, "File name already in use", err)
}
}
result, err := s.store.Write(ctx, &entity.WriteEntityRequest{
GRN: grn,
Body: data,
Comment: message,
Folder: folder,
// 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 *httpEntityStore) doListFolder(c *contextmodel.ReqContext) response.Response {
return response.JSON(501, "Not implemented yet")
}
func (s *httpEntityStore) doSearch(c *contextmodel.ReqContext) response.Response {
vals := c.Req.URL.Query()
req := &entity.EntitySearchRequest{
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]
}