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",
|
"distSpecVersion": "1.1.0",
|
||||||
"storage": {
|
"storage": {
|
||||||
"rootDirectory": "/tmp/zot0",
|
"rootDirectory": "/workspace/zot/data/mem1",
|
||||||
"dedupe": false
|
"dedupe": false
|
||||||
},
|
},
|
||||||
"http": {
|
"http": {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"distSpecVersion": "1.1.0",
|
"distSpecVersion": "1.1.0",
|
||||||
"storage": {
|
"storage": {
|
||||||
"rootDirectory": "/tmp/zot1",
|
"rootDirectory": "/workspace/zot/data/mem2",
|
||||||
"dedupe": false
|
"dedupe": false
|
||||||
},
|
},
|
||||||
"http": {
|
"http": {
|
||||||
|
|
|
@ -92,17 +92,18 @@ func SetupSearchRoutes(conf *config.Config, router *mux.Router, storeController
|
||||||
}
|
}
|
||||||
|
|
||||||
resConfig := search.GetResolverConfig(log, storeController, metaDB, cveInfo)
|
resConfig := search.GetResolverConfig(log, storeController, metaDB, cveInfo)
|
||||||
|
executableSchema := gql_generated.NewExecutableSchema(resConfig)
|
||||||
|
|
||||||
allowedMethods := zcommon.AllowedMethods(http.MethodGet, http.MethodPost)
|
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 := router.PathPrefix(constants.ExtSearchPrefix).Subrouter()
|
||||||
extRouter.Use(zcommon.CORSHeadersMiddleware(conf.HTTP.AllowOrigin))
|
extRouter.Use(zcommon.CORSHeadersMiddleware(conf.HTTP.AllowOrigin))
|
||||||
extRouter.Use(zcommon.ACHeadersMiddleware(conf, allowedMethods...))
|
extRouter.Use(zcommon.ACHeadersMiddleware(conf, allowedMethods...))
|
||||||
extRouter.Use(zcommon.AddExtensionSecurityHeaders())
|
extRouter.Use(zcommon.AddExtensionSecurityHeaders())
|
||||||
extRouter.Methods(allowedMethods...).
|
extRouter.Methods(allowedMethods...).
|
||||||
Handler(gqlProxy(gqlHandler.NewDefaultServer(gql_generated.NewExecutableSchema(resConfig))))
|
Handler(gqlProxy(gqlHandler.NewDefaultServer(executableSchema)))
|
||||||
|
|
||||||
log.Info().Msg("finished setting up search routes")
|
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 {
|
if err != nil {
|
||||||
log.Error().
|
log.Error().
|
||||||
Str(LoggerFieldOperation, HandlerOperation).
|
Str(LoggerFieldOperation, HandlerOperation).
|
||||||
|
@ -106,6 +106,8 @@ func HandleGlobalSearchResult(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
response.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
_, err = response.Write(responseBody)
|
_, err = response.Write(responseBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().
|
log.Error().
|
||||||
|
|
|
@ -2,7 +2,9 @@ package gqlproxy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"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/config"
|
||||||
"zotregistry.dev/zot/pkg/api/constants"
|
"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
|
// 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,
|
// 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.
|
// 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 func(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
|
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
|
||||||
// If not running in cluster mode, no op.
|
// If not running in cluster mode, no op.
|
||||||
|
@ -37,28 +43,40 @@ func GqlProxyRequestHandler(config *config.Config, log log.Logger) func(handler
|
||||||
return
|
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")
|
query := request.URL.Query().Get("query")
|
||||||
|
|
||||||
operation, ok := computeGqlOperation(query)
|
// Load the query using gqlparser.
|
||||||
if !ok {
|
// 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")
|
log.Error().Str("query", query).Msg("Failed to compute operation from query")
|
||||||
http.Error(response, "Failed to process GQL request", http.StatusInternalServerError)
|
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) {
|
func TestGlobalSearchWithScaleOutProxyLocalStorage(t *testing.T) {
|
||||||
|
|
||||||
// When there are 2 zot instances, the same GlobalSearch query should
|
// When there are 2 zot instances, the same GlobalSearch query should
|
||||||
// return aggregated data from both instances when both instances are queried.
|
// 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() {
|
Convey("In a local scale-out cluster with 2 members, should return correct data for GlobalSearch", t, func() {
|
||||||
|
|
Loading…
Reference in a new issue