Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add /admin/schema endpoint #4777

Merged
merged 10 commits into from
Feb 14, 2020
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 58 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 @@ -591,7 +579,59 @@ func alterHandler(w http.ResponseWriter, r *http.Request) {
return
}

_, _ = writeResponse(w, r, js)
_, _ = x.WriteResponse(w, r, js)
}

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
}

res := map[string]interface{}{}
data := map[string]interface{}{}
data["code"] = x.Success
data["message"] = "Done"
res["data"] = data

js, err := json.Marshal(res)
if err != nil {
x.SetStatus(w, x.Error, err.Error())
return
}

_, _ = 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 @@ -468,6 +468,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 @@ -680,3 +681,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.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