0
Fork 0
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:
Vishwas R 2024-09-28 15:51:04 +00:00 committed by Vishwas Rajashekar
parent 536ecde31a
commit bcf52c835d
No known key found for this signature in database
6 changed files with 48 additions and 41 deletions

View file

@ -1,7 +1,7 @@
{
"distSpecVersion": "1.1.0",
"storage": {
"rootDirectory": "/tmp/zot0",
"rootDirectory": "/workspace/zot/data/mem1",
"dedupe": false
},
"http": {

View file

@ -1,7 +1,7 @@
{
"distSpecVersion": "1.1.0",
"storage": {
"rootDirectory": "/tmp/zot1",
"rootDirectory": "/workspace/zot/data/mem2",
"dedupe": false
},
"http": {

View file

@ -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")
}

View file

@ -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().

View file

@ -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
}

View file

@ -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() {