diff --git a/pkg/api/api.go b/pkg/api/api.go index ee3c5a828a6..cd639f935b6 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -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) } diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 412facb7cfb..1a9d45d149a 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -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, diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 95db107db6d..61bebfdf71f 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -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, diff --git a/pkg/services/store/entity/httpentitystore/service.go b/pkg/services/store/entity/httpentitystore/service.go deleted file mode 100644 index 6b992f50f2e..00000000000 --- a/pkg/services/store/entity/httpentitystore/service.go +++ /dev/null @@ -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] -}