From 6e30c1906f0ef195de9293cd23791fd6a5afbc76 Mon Sep 17 00:00:00 2001 From: Abhimanyu Singh Gaur <12651351+abhimanyusinghgaur@users.noreply.github.com> Date: Fri, 14 Feb 2020 22:04:09 +0530 Subject: [PATCH] add /admin/schema endpoint (#4777) Adds a POST `/admin/schema` endpoint which allows updating the GraphQL schema similar to how `/alter` works. --- dgraph/cmd/alpha/http.go | 68 +++++++++++++++++++-------- dgraph/cmd/alpha/login_ee.go | 2 +- dgraph/cmd/alpha/run.go | 3 ++ graphql/e2e/common/admin.go | 89 ++++++++++++++++++++++++++++++++++++ graphql/e2e/common/common.go | 39 ++++++++++++++-- graphql/web/http.go | 12 +++-- x/x.go | 16 +++++++ 7 files changed, 203 insertions(+), 26 deletions(-) diff --git a/dgraph/cmd/alpha/http.go b/dgraph/cmd/alpha/http.go index 468efee5c30..1c5e47966ac 100644 --- a/dgraph/cmd/alpha/http.go +++ b/dgraph/cmd/alpha/http.go @@ -21,6 +21,8 @@ import ( "compress/gzip" "context" "encoding/json" + "github.com/dgraph-io/dgraph/graphql/schema" + "github.com/dgraph-io/dgraph/graphql/web" "io" "io/ioutil" "net/http" @@ -143,20 +145,6 @@ func parseDuration(r *http.Request, name string) (time.Duration, error) { return durationValue, nil } -// Write response body, transparently compressing if necessary. -func writeResponse(w http.ResponseWriter, r *http.Request, b []byte) (int, error) { - var out io.Writer = w - - if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { - w.Header().Set("Content-Encoding", "gzip") - gzw := gzip.NewWriter(w) - defer gzw.Close() - out = gzw - } - - return out.Write(b) -} - // This method should just build the request and proxy it to the Query method of dgraph.Server. // It can then encode the response as appropriate before sending it back to the user. func queryHandler(w http.ResponseWriter, r *http.Request) { @@ -277,7 +265,7 @@ func queryHandler(w http.ResponseWriter, r *http.Request) { writeEntry("extensions", js) x.Check2(out.WriteRune('}')) - if _, err := writeResponse(w, r, out.Bytes()); err != nil { + if _, err := x.WriteResponse(w, r, out.Bytes()); err != nil { // If client crashes before server could write response, writeResponse will error out, // Check2 will fatal and shut the server down in such scenario. We don't want that. glog.Errorln("Unable to write response: ", err) @@ -432,7 +420,7 @@ func mutationHandler(w http.ResponseWriter, r *http.Request) { return } - _, _ = writeResponse(w, r, js) + _, _ = x.WriteResponse(w, r, js) } func commitHandler(w http.ResponseWriter, r *http.Request) { @@ -480,7 +468,7 @@ func commitHandler(w http.ResponseWriter, r *http.Request) { return } - _, _ = writeResponse(w, r, js) + _, _ = x.WriteResponse(w, r, js) } func handleAbort(startTs uint64) (map[string]interface{}, error) { @@ -579,6 +567,50 @@ func alterHandler(w http.ResponseWriter, r *http.Request) { return } + writeSuccessResponse(w, r) +} + +func adminSchemaHandler(w http.ResponseWriter, r *http.Request, adminServer web.IServeGraphQL) { + if commonHandler(w, r) { + return + } + + b := readRequest(w, r) + if b == nil { + return + } + + md := metadata.New(nil) + ctx := metadata.NewIncomingContext(context.Background(), md) + ctx = x.AttachAccessJwt(ctx, r) + + gqlReq := &schema.Request{} + gqlReq.Query = ` + mutation updateGqlSchema($sch: String!) { + updateGQLSchema(input: { + set: { + schema: $sch + } + }) { + gqlSchema { + id + } + } + }` + gqlReq.Variables = map[string]interface{}{ + "sch": string(b), + } + + response := adminServer.Resolve(ctx, gqlReq) + if len(response.Errors) > 0 { + x.SetStatus(w, x.Error, response.Errors.Error()) + return + } + + writeSuccessResponse(w, r) +} + +func writeSuccessResponse(w http.ResponseWriter, r *http.Request) { res := map[string]interface{}{} data := map[string]interface{}{} data["code"] = x.Success @@ -591,7 +623,7 @@ func alterHandler(w http.ResponseWriter, r *http.Request) { return } - _, _ = writeResponse(w, r, js) + _, _ = x.WriteResponse(w, r, js) } // skipJSONUnmarshal stores the raw bytes as is while JSON unmarshaling. diff --git a/dgraph/cmd/alpha/login_ee.go b/dgraph/cmd/alpha/login_ee.go index 67a19a6f502..df3f6e6e5e1 100644 --- a/dgraph/cmd/alpha/login_ee.go +++ b/dgraph/cmd/alpha/login_ee.go @@ -80,7 +80,7 @@ func loginHandler(w http.ResponseWriter, r *http.Request) { return } - if _, err := writeResponse(w, r, js); err != nil { + if _, err := x.WriteResponse(w, r, js); err != nil { glog.Errorf("Error while writing response: %v", err) } } diff --git a/dgraph/cmd/alpha/run.go b/dgraph/cmd/alpha/run.go index 0ec94074dde..04572b2fb8f 100644 --- a/dgraph/cmd/alpha/run.go +++ b/dgraph/cmd/alpha/run.go @@ -463,6 +463,9 @@ func setupServer(closer *y.Closer) { }) } http.Handle("/admin", whitelist(adminServer.HTTPHandler())) + http.HandleFunc("/admin/schema", func(w http.ResponseWriter, r *http.Request) { + adminSchemaHandler(w, r, adminServer) + }) addr := fmt.Sprintf("%s:%d", laddr, httpPort()) glog.Infof("Bringing up GraphQL HTTP API at %s/graphql", addr) diff --git a/graphql/e2e/common/admin.go b/graphql/e2e/common/admin.go index 49d5c50bcec..e6c1cd85583 100644 --- a/graphql/e2e/common/admin.go +++ b/graphql/e2e/common/admin.go @@ -181,6 +181,82 @@ const ( ] } }` + + adminSchemaEndptTypes = ` + type A { + b: String + c: Int + d: Float + }` + adminSchemaEndptSchema = `{ + "schema": [ + { + "predicate": "A.b", + "type": "string" + }, + { + "predicate": "A.c", + "type": "int" + }, + { + "predicate": "A.d", + "type": "float" + }, + { + "predicate": "dgraph.graphql.schema", + "type": "string" + }, + { + "predicate": "dgraph.type", + "type": "string", + "index": true, + "tokenizer": [ + "exact" + ], + "list": true + } + ], + "types": [ + { + "fields": [ + { + "name": "A.b" + }, + { + "name": "A.c" + }, + { + "name": "A.d" + } + ], + "name": "A" + }, + { + "fields": [ + { + "name": "dgraph.graphql.schema" + } + ], + "name": "dgraph.graphql" + } + ] +}` + adminSchemaEndptGQLSchema = `{ + "__type": { + "name": "A", + "fields": [ + { + "name": "b" + }, + { + "name": "c" + }, + { + "name": "d" + } + ] + } +}` ) func admin(t *testing.T) { @@ -196,6 +272,7 @@ func admin(t *testing.T) { schemaIsInInitialState(t, client) addGQLSchema(t, client) updateSchema(t, client) + updateSchemaThroughAdminSchemaEndpt(t, client) } func schemaIsInInitialState(t *testing.T, client *dgo.Dgraph) { @@ -229,6 +306,18 @@ func updateSchema(t *testing.T, client *dgo.Dgraph) { introspect(t, updatedGQLSchema) } +func updateSchemaThroughAdminSchemaEndpt(t *testing.T, client *dgo.Dgraph) { + err := addSchemaThroughAdminSchemaEndpt(graphqlAdminTestAdminSchemaURL, adminSchemaEndptTypes) + require.NoError(t, err) + + resp, err := client.NewReadOnlyTxn().Query(context.Background(), "schema {}") + require.NoError(t, err) + + require.JSONEq(t, adminSchemaEndptSchema, string(resp.GetJson())) + + introspect(t, adminSchemaEndptGQLSchema) +} + func introspect(t *testing.T, expected string) { queryParams := &GraphQLParams{ Query: `query { diff --git a/graphql/e2e/common/common.go b/graphql/e2e/common/common.go index 3a10f6efffa..3c2739da201 100644 --- a/graphql/e2e/common/common.go +++ b/graphql/e2e/common/common.go @@ -42,10 +42,11 @@ const ( graphqlAdminURL = "http://localhost:8180/admin" alphagRPC = "localhost:9180" - adminDgraphHealthURL = "http://localhost:8280/health?all" - graphqlAdminTestURL = "http://localhost:8280/graphql" - graphqlAdminTestAdminURL = "http://localhost:8280/admin" - alphaAdminTestgRPC = "localhost:9280" + adminDgraphHealthURL = "http://localhost:8280/health?all" + graphqlAdminTestURL = "http://localhost:8280/graphql" + graphqlAdminTestAdminURL = "http://localhost:8280/admin" + graphqlAdminTestAdminSchemaURL = "http://localhost:8280/admin/schema" + alphaAdminTestgRPC = "localhost:9280" ) // GraphQLParams is parameters for the constructing a GraphQL query - that's @@ -681,3 +682,33 @@ func addSchema(url string, schema string) error { return nil } + +func addSchemaThroughAdminSchemaEndpt(url string, schema string) error { + req, err := http.NewRequest(http.MethodPost, url, strings.NewReader(schema)) + if err != nil { + return errors.Wrap(err, "error running GraphQL query") + } + + resp, err := runGQLRequest(req) + if err != nil { + return errors.Wrap(err, "error running GraphQL query") + } + + var addResult struct { + Data struct { + Code string + Message string + } + } + + err = json.Unmarshal(resp, &addResult) + if err != nil { + return errors.Wrap(err, "error trying to unmarshal GraphQL mutation result") + } + + if addResult.Data.Code != "Success" && addResult.Data.Message != "Done" { + return errors.New("GraphQL schema mutation failed") + } + + return nil +} diff --git a/graphql/web/http.go b/graphql/web/http.go index b1531f69ac5..fb805545f17 100644 --- a/graphql/web/http.go +++ b/graphql/web/http.go @@ -43,6 +43,9 @@ type IServeGraphQL interface { // HTTPHandler returns a http.Handler that serves GraphQL. HTTPHandler() http.Handler + + // Resolve processes a GQL Request using the correct resolver and returns a GQL Response + Resolve(ctx context.Context, gqlReq *schema.Request) *schema.Response } type graphqlHandler struct { @@ -65,6 +68,10 @@ func (gh *graphqlHandler) ServeGQL(resolver *resolve.RequestResolver) { gh.resolver = resolver } +func (gh *graphqlHandler) Resolve(ctx context.Context, gqlReq *schema.Request) *schema.Response { + return gh.resolver.Resolve(ctx, gqlReq) +} + // write chooses between the http response writer and gzip writer // and sends the schema response using that. func write(w http.ResponseWriter, rr *schema.Response, acceptGzip bool) { @@ -96,11 +103,11 @@ func (gh *graphqlHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { panic("graphqlHandler not initialised") } + ctx = x.AttachAccessJwt(ctx, r) + var res *schema.Response gqlReq, err := getRequest(ctx, r) - ctx = x.AttachAccessJwt(ctx, r) - if err != nil { res = schema.ErrorResponse(err) } else { @@ -108,7 +115,6 @@ func (gh *graphqlHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } write(w, res, strings.Contains(r.Header.Get("Accept-Encoding"), "gzip")) - } func (gh *graphqlHandler) isValid() bool { diff --git a/x/x.go b/x/x.go index db4b851849c..f9fea0788ee 100644 --- a/x/x.go +++ b/x/x.go @@ -19,10 +19,12 @@ package x import ( "bufio" "bytes" + builtinGzip "compress/gzip" "context" "crypto/tls" "encoding/json" "fmt" + "io" "math" "math/rand" "net" @@ -338,6 +340,20 @@ func AttachAccessJwt(ctx context.Context, r *http.Request) context.Context { return ctx } +// Write response body, transparently compressing if necessary. +func WriteResponse(w http.ResponseWriter, r *http.Request, b []byte) (int, error) { + var out io.Writer = w + + if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { + w.Header().Set("Content-Encoding", "gzip") + gzw := builtinGzip.NewWriter(w) + defer gzw.Close() + out = gzw + } + + return out.Write(b) +} + // Min returns the minimum of the two given numbers. func Min(a, b uint64) uint64 { if a < b {