Skip to content

Commit

Permalink
add /admin/schema endpoint (#4777)
Browse files Browse the repository at this point in the history
Adds a POST `/admin/schema` endpoint which allows updating the GraphQL schema similar to how `/alter` works.
  • Loading branch information
abhimanyusinghgaur authored Feb 14, 2020
1 parent 8fa2fd9 commit 6e30c19
Show file tree
Hide file tree
Showing 7 changed files with 203 additions and 26 deletions.
68 changes: 50 additions & 18 deletions dgraph/cmd/alpha/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion dgraph/cmd/alpha/login_ee.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand Down
3 changes: 3 additions & 0 deletions dgraph/cmd/alpha/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
89 changes: 89 additions & 0 deletions graphql/e2e/common/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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 {
Expand Down
39 changes: 35 additions & 4 deletions graphql/e2e/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
12 changes: 9 additions & 3 deletions graphql/web/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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) {
Expand Down Expand Up @@ -96,19 +103,18 @@ 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 {
res = gh.resolver.Resolve(ctx, gqlReq)
}

write(w, res, strings.Contains(r.Header.Get("Accept-Encoding"), "gzip"))

}

func (gh *graphqlHandler) isValid() bool {
Expand Down
16 changes: 16 additions & 0 deletions x/x.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@ package x
import (
"bufio"
"bytes"
builtinGzip "compress/gzip"
"context"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"math"
"math/rand"
"net"
Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit 6e30c19

Please sign in to comment.