mirror of
https://github.com/project-zot/zot.git
synced 2024-12-16 21:56:37 -05:00
feat(scale-out): use gqlparser for parsing incoming query
Signed-off-by: Vishwas Rajashekar <vrajashe@cisco.com>
This commit is contained in:
parent
536ecde31a
commit
bcf52c835d
6 changed files with 48 additions and 41 deletions
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"distSpecVersion": "1.1.0",
|
||||
"storage": {
|
||||
"rootDirectory": "/tmp/zot0",
|
||||
"rootDirectory": "/workspace/zot/data/mem1",
|
||||
"dedupe": false
|
||||
},
|
||||
"http": {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"distSpecVersion": "1.1.0",
|
||||
"storage": {
|
||||
"rootDirectory": "/tmp/zot1",
|
||||
"rootDirectory": "/workspace/zot/data/mem2",
|
||||
"dedupe": false
|
||||
},
|
||||
"http": {
|
||||
|
|
|
@ -92,17 +92,18 @@ func SetupSearchRoutes(conf *config.Config, router *mux.Router, storeController
|
|||
}
|
||||
|
||||
resConfig := search.GetResolverConfig(log, storeController, metaDB, cveInfo)
|
||||
executableSchema := gql_generated.NewExecutableSchema(resConfig)
|
||||
|
||||
allowedMethods := zcommon.AllowedMethods(http.MethodGet, http.MethodPost)
|
||||
|
||||
gqlProxy := gqlproxy.GqlProxyRequestHandler(conf, log)
|
||||
gqlProxy := gqlproxy.GqlProxyRequestHandler(conf, log, executableSchema.Schema())
|
||||
|
||||
extRouter := router.PathPrefix(constants.ExtSearchPrefix).Subrouter()
|
||||
extRouter.Use(zcommon.CORSHeadersMiddleware(conf.HTTP.AllowOrigin))
|
||||
extRouter.Use(zcommon.ACHeadersMiddleware(conf, allowedMethods...))
|
||||
extRouter.Use(zcommon.AddExtensionSecurityHeaders())
|
||||
extRouter.Methods(allowedMethods...).
|
||||
Handler(gqlProxy(gqlHandler.NewDefaultServer(gql_generated.NewExecutableSchema(resConfig))))
|
||||
Handler(gqlProxy(gqlHandler.NewDefaultServer(executableSchema)))
|
||||
|
||||
log.Info().Msg("finished setting up search routes")
|
||||
}
|
||||
|
|
|
@ -96,7 +96,7 @@ func HandleGlobalSearchResult(
|
|||
)
|
||||
}
|
||||
|
||||
responseBody, err := json.Marshal(collatedResult)
|
||||
responseBody, err := json.MarshalIndent(collatedResult, "", " ")
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Str(LoggerFieldOperation, HandlerOperation).
|
||||
|
@ -106,6 +106,8 @@ func HandleGlobalSearchResult(
|
|||
return
|
||||
}
|
||||
|
||||
response.Header().Set("Content-Type", "application/json")
|
||||
|
||||
_, err = response.Write(responseBody)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
|
|
|
@ -2,7 +2,9 @@ package gqlproxy
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/vektah/gqlparser/v2"
|
||||
"github.com/vektah/gqlparser/v2/ast"
|
||||
|
||||
"zotregistry.dev/zot/pkg/api/config"
|
||||
"zotregistry.dev/zot/pkg/api/constants"
|
||||
|
@ -14,7 +16,11 @@ import (
|
|||
// Requests are only proxied in local cluster mode as in this mode, each instance holds only the
|
||||
// metadata for the images that it serves, however, in shared storage mode,
|
||||
// all the instances have access to all the metadata so any can respond.
|
||||
func GqlProxyRequestHandler(config *config.Config, log log.Logger) func(handler http.Handler) http.Handler {
|
||||
func GqlProxyRequestHandler(
|
||||
config *config.Config,
|
||||
log log.Logger,
|
||||
gqlSchema *ast.Schema,
|
||||
) func(handler http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
|
||||
// If not running in cluster mode, no op.
|
||||
|
@ -37,28 +43,40 @@ func GqlProxyRequestHandler(config *config.Config, log log.Logger) func(handler
|
|||
return
|
||||
}
|
||||
|
||||
// General Structure for GQL Requests
|
||||
// Query String contains the full GraphQL Request (it's NOT JSON)
|
||||
// e.g. {(query:"", requestedPage: {limit:3 offset:0 sortBy: DOWNLOADS} )
|
||||
// {Page {TotalCount ItemCount} Repos {Name LastUpdated Size Platforms { Os Arch }
|
||||
// IsStarred IsBookmarked NewestImage { Tag Vulnerabilities {MaxSeverity Count}
|
||||
// Description IsSigned SignGlobalSearchatureInfo { Tool IsTrusted Author } Licenses Vendor Labels }
|
||||
// StarCount DownloadCount}}}
|
||||
|
||||
// General Payload Structure for GQL Response
|
||||
/*
|
||||
{
|
||||
"errors": [CUSTOM_ERRORS_HERE],
|
||||
"data": {
|
||||
"NameOfQuery": {CUSTOM_SCHEMA_HERE}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
query := request.URL.Query().Get("query")
|
||||
|
||||
operation, ok := computeGqlOperation(query)
|
||||
if !ok {
|
||||
// Load the query using gqlparser.
|
||||
// This helps to read the Operation correctly which is in turn used to
|
||||
// dynamically hand-off the processing to the appropriate handler.
|
||||
processedGql, errList := gqlparser.LoadQuery(gqlSchema, query)
|
||||
|
||||
if len(errList) != 0 {
|
||||
for _, err := range errList {
|
||||
log.Error().Str("query", query).Err(err).Msg(err.Message)
|
||||
}
|
||||
http.Error(response, "Failed to process GQL request", http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Look at the first operation in the query.
|
||||
// TODO: for completeness, this should support multiple
|
||||
// operations at once.
|
||||
operation := ""
|
||||
for _, op := range processedGql.Operations {
|
||||
for _, ss := range op.SelectionSet {
|
||||
switch ss := ss.(type) {
|
||||
case *ast.Field:
|
||||
operation = ss.Name
|
||||
default:
|
||||
log.Error().Str("query", query).Msg("Unsupported type")
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if operation == "" {
|
||||
log.Error().Str("query", query).Msg("Failed to compute operation from query")
|
||||
http.Error(response, "Failed to process GQL request", http.StatusInternalServerError)
|
||||
|
||||
|
@ -80,16 +98,3 @@ func GqlProxyRequestHandler(config *config.Config, log log.Logger) func(handler
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Naively compute which operation is requested for GQL.
|
||||
// TODO: Need to replace this with better custom GQL parsing
|
||||
// or a parsing library that can conver the GQL query to
|
||||
// a struct where operations data and schema are available for reading.
|
||||
func computeGqlOperation(request string) (string, bool) {
|
||||
openParenthesisIndex := strings.Index(request, "(")
|
||||
if openParenthesisIndex == -1 {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return request[1:openParenthesisIndex], true
|
||||
}
|
||||
|
|
|
@ -3916,7 +3916,6 @@ func TestGlobalSearch(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestGlobalSearchWithScaleOutProxyLocalStorage(t *testing.T) {
|
||||
|
||||
// When there are 2 zot instances, the same GlobalSearch query should
|
||||
// return aggregated data from both instances when both instances are queried.
|
||||
Convey("In a local scale-out cluster with 2 members, should return correct data for GlobalSearch", t, func() {
|
||||
|
|
Loading…
Reference in a new issue