github.com/dgraph-io/dgraph@v1.2.8/graphql/admin/admin.go (about)

     1  /*
     2   * Copyright 2019 Dgraph Labs, Inc. and Contributors
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *     http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  package admin
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"fmt"
    23  	"sync"
    24  	"time"
    25  
    26  	"github.com/golang/glog"
    27  	"github.com/pkg/errors"
    28  
    29  	badgerpb "github.com/dgraph-io/badger/v2/pb"
    30  	dgoapi "github.com/dgraph-io/dgo/v2/protos/api"
    31  	"github.com/dgraph-io/dgraph/edgraph"
    32  	"github.com/dgraph-io/dgraph/graphql/resolve"
    33  	"github.com/dgraph-io/dgraph/graphql/schema"
    34  	"github.com/dgraph-io/dgraph/graphql/web"
    35  	"github.com/dgraph-io/dgraph/protos/pb"
    36  	"github.com/dgraph-io/dgraph/worker"
    37  	"github.com/dgraph-io/dgraph/x"
    38  	"github.com/dgraph-io/ristretto/z"
    39  )
    40  
    41  const (
    42  	errMsgServerNotReady = "Unavailable: Server not ready."
    43  
    44  	errNoGraphQLSchema = "Not resolving %s. There's no GraphQL schema in Dgraph.  " +
    45  		"Use the /admin API to add a GraphQL schema"
    46  	errResolverNotFound = "%s was not executed because no suitable resolver could be found - " +
    47  		"this indicates a resolver or validation bug " +
    48  		"(Please let us know : https://github.com/dgraph-io/dgraph/issues)"
    49  
    50  	// The schema fragment that's needed in Dgraph to operate the GraphQL layer.
    51  	dgraphAdminSchema = `
    52  	type dgraph.graphql {
    53  		dgraph.graphql.schema
    54  	}`
    55  
    56  	// GraphQL schema for /admin endpoint.
    57  	//
    58  	// Eventually we should generate this from just the types definition.
    59  	// But for now, that would add too much into the schema, so this is
    60  	// hand crafted to be one of our schemas so we can pass it into the
    61  	// pipeline.
    62  	graphqlAdminSchema = `
    63  	type GQLSchema @dgraph(type: "dgraph.graphql") {
    64  		id: ID!
    65  		schema: String!  @dgraph(type: "dgraph.graphql.schema") 
    66  		generatedSchema: String!
    67  	}
    68  
    69  	type Health {
    70  		message: String!
    71  		status: HealthStatus!
    72  	}
    73  
    74  	enum HealthStatus {
    75  		ErrNoConnection
    76  		NoGraphQLSchema
    77  		Healthy
    78  	}
    79  
    80  	scalar DateTime
    81  
    82  	directive @dgraph(type: String, pred: String) on OBJECT | INTERFACE | FIELD_DEFINITION
    83  
    84  	type UpdateGQLSchemaPayload {
    85  		gqlSchema: GQLSchema
    86  	}
    87  
    88  	input UpdateGQLSchemaInput {
    89  		set: GQLSchemaPatch!
    90  	}
    91  
    92  	input GQLSchemaPatch {
    93  		schema: String!
    94  	}
    95  
    96  	type Query {
    97  		getGQLSchema: GQLSchema
    98  		health: Health
    99  	}
   100  
   101  	type Mutation {
   102  		updateGQLSchema(input: UpdateGQLSchemaInput!) : UpdateGQLSchemaPayload
   103  	}
   104   `
   105  )
   106  
   107  type gqlSchema struct {
   108  	ID              string `json:"id,omitempty"`
   109  	Schema          string `json:"schema,omitempty"`
   110  	GeneratedSchema string
   111  }
   112  
   113  type adminServer struct {
   114  	rf       resolve.ResolverFactory
   115  	resolver *resolve.RequestResolver
   116  	status   healthStatus
   117  
   118  	// The mutex that locks schema update operations
   119  	mux sync.Mutex
   120  
   121  	// The GraphQL server that's being admin'd
   122  	gqlServer web.IServeGraphQL
   123  
   124  	schema gqlSchema
   125  
   126  	// When the schema changes, we use these to create a new RequestResolver for
   127  	// the main graphql endpoint (gqlServer) and thus refresh the API.
   128  	fns               *resolve.ResolverFns
   129  	withIntrospection bool
   130  }
   131  
   132  // NewServers initializes the GraphQL servers.  It sets up an empty server for the
   133  // main /graphql endpoint and an admin server.  The result is mainServer, adminServer.
   134  func NewServers(withIntrospection bool, closer *z.Closer) (web.IServeGraphQL, web.IServeGraphQL) {
   135  
   136  	gqlSchema, err := schema.FromString("")
   137  	if err != nil {
   138  		panic(err)
   139  	}
   140  
   141  	resolvers := resolve.New(gqlSchema, resolverFactoryWithErrorMsg(errNoGraphQLSchema))
   142  	mainServer := web.NewServer(resolvers)
   143  
   144  	fns := &resolve.ResolverFns{
   145  		Qrw: resolve.NewQueryRewriter(),
   146  		Arw: resolve.NewAddRewriter,
   147  		Urw: resolve.NewUpdateRewriter,
   148  		Drw: resolve.NewDeleteRewriter(),
   149  	}
   150  	adminResolvers := newAdminResolver(mainServer, fns, withIntrospection, closer)
   151  	adminServer := web.NewServer(adminResolvers)
   152  
   153  	return mainServer, adminServer
   154  }
   155  
   156  // newAdminResolver creates a GraphQL request resolver for the /admin endpoint.
   157  func newAdminResolver(
   158  	gqlServer web.IServeGraphQL,
   159  	fns *resolve.ResolverFns,
   160  	withIntrospection bool,
   161  	closer *z.Closer) *resolve.RequestResolver {
   162  
   163  	adminSchema, err := schema.FromString(graphqlAdminSchema)
   164  	if err != nil {
   165  		panic(err)
   166  	}
   167  
   168  	rf := newAdminResolverFactory()
   169  
   170  	server := &adminServer{
   171  		rf:                rf,
   172  		resolver:          resolve.New(adminSchema, rf),
   173  		status:            errNoConnection,
   174  		gqlServer:         gqlServer,
   175  		fns:               fns,
   176  		withIntrospection: withIntrospection,
   177  	}
   178  
   179  	prefix := x.DataKey("dgraph.graphql.schema", 0)
   180  	// Remove uid from the key, to get the correct prefix
   181  	prefix = prefix[:len(prefix)-8]
   182  	// Listen for graphql schema changes in group 1.
   183  	go worker.SubscribeForUpdates([][]byte{prefix}, func(kvs *badgerpb.KVList) {
   184  		// Last update contains the latest value. So, taking the last update.
   185  		lastIdx := len(kvs.GetKv()) - 1
   186  		kv := kvs.GetKv()[lastIdx]
   187  
   188  		// Unmarshal the incoming posting list.
   189  		pl := &pb.PostingList{}
   190  		err := pl.Unmarshal(kv.GetValue())
   191  		if err != nil {
   192  			glog.Errorf("Unable to unmarshal the posting list for graphql schema update %s", err)
   193  			return
   194  		}
   195  
   196  		// There should be only one posting.
   197  		if len(pl.Postings) != 1 {
   198  			glog.Errorf("Only one posting is expected in the graphql schema posting list but got %d",
   199  				len(pl.Postings))
   200  			return
   201  		}
   202  
   203  		pk, err := x.Parse(kv.GetKey())
   204  		if err != nil {
   205  			glog.Errorf("Unable to find uid of updated schema %s", err)
   206  			return
   207  		}
   208  
   209  		newSchema := gqlSchema{
   210  			ID:     fmt.Sprintf("%#x", pk.Uid),
   211  			Schema: string(pl.Postings[0].Value),
   212  		}
   213  
   214  		schHandler, err := schema.NewHandler(newSchema.Schema)
   215  		if err != nil {
   216  			glog.Errorf("Error processing GraphQL schema: %s.  ", err)
   217  			return
   218  		}
   219  
   220  		newSchema.GeneratedSchema = schHandler.GQLSchema()
   221  		gqlSchema, err := schema.FromString(newSchema.GeneratedSchema)
   222  		if err != nil {
   223  			glog.Errorf("Error processing GraphQL schema: %s.  ", err)
   224  			return
   225  		}
   226  
   227  		glog.Infof("Successfully updated GraphQL schema.")
   228  
   229  		server.mux.Lock()
   230  		defer server.mux.Unlock()
   231  
   232  		server.schema = newSchema
   233  		server.status = healthy
   234  		server.resetSchema(gqlSchema)
   235  	}, 1, closer)
   236  
   237  	go server.initServer()
   238  
   239  	return server.resolver
   240  }
   241  
   242  func newAdminResolverFactory() resolve.ResolverFactory {
   243  	rf := resolverFactoryWithErrorMsg(errResolverNotFound).
   244  		WithQueryResolver("health",
   245  			func(q schema.Query) resolve.QueryResolver {
   246  				health := &healthResolver{
   247  					status: errNoConnection,
   248  				}
   249  
   250  				return resolve.NewQueryResolver(
   251  					health,
   252  					health,
   253  					resolve.StdQueryCompletion())
   254  			}).
   255  		WithMutationResolver("updateGQLSchema", func(m schema.Mutation) resolve.MutationResolver {
   256  			return resolve.MutationResolverFunc(
   257  				func(ctx context.Context, m schema.Mutation) (*resolve.Resolved, bool) {
   258  					return &resolve.Resolved{Err: errors.Errorf(errMsgServerNotReady)}, false
   259  				})
   260  		}).
   261  		WithQueryResolver("getGQLSchema", func(q schema.Query) resolve.QueryResolver {
   262  			return resolve.QueryResolverFunc(
   263  				func(ctx context.Context, query schema.Query) *resolve.Resolved {
   264  					return &resolve.Resolved{Err: errors.Errorf(errMsgServerNotReady)}
   265  				})
   266  		}).
   267  		WithSchemaIntrospection()
   268  
   269  	return rf
   270  }
   271  
   272  func (as *adminServer) initServer() {
   273  	var waitFor time.Duration
   274  	for {
   275  		<-time.After(waitFor)
   276  		waitFor = 10 * time.Second
   277  
   278  		err := checkAdminSchemaExists()
   279  		if err != nil {
   280  			if glog.V(3) {
   281  				glog.Infof("Failed checking GraphQL admin schema: %s.  Trying again in %f seconds",
   282  					err, waitFor.Seconds())
   283  			}
   284  			continue
   285  		}
   286  
   287  		// Nothing else should be able to lock before here.  The admin resolvers aren't yet
   288  		// set up (they all just error), so we will obtain the lock here without contention.
   289  		// We then setup the admin resolvers and they must wait until we are done before the
   290  		// first admin calls will go through.
   291  		as.mux.Lock()
   292  		defer as.mux.Unlock()
   293  
   294  		as.addConnectedAdminResolvers()
   295  
   296  		as.status = noGraphQLSchema
   297  
   298  		sch, err := getCurrentGraphQLSchema(as.resolver)
   299  		if err != nil {
   300  			glog.Infof("Error reading GraphQL schema: %s.  "+
   301  				"Admin server is connected, but no GraphQL schema is being served.", err)
   302  			break
   303  		} else if sch == nil {
   304  			glog.Infof("No GraphQL schema in Dgraph; serving empty GraphQL API")
   305  			break
   306  		}
   307  
   308  		schHandler, err := schema.NewHandler(sch.Schema)
   309  		if err != nil {
   310  			glog.Infof("Error processing GraphQL schema: %s.  "+
   311  				"Admin server is connected, but no GraphQL schema is being served.", err)
   312  			break
   313  		}
   314  
   315  		sch.GeneratedSchema = schHandler.GQLSchema()
   316  		generatedSchema, err := schema.FromString(sch.GeneratedSchema)
   317  		if err != nil {
   318  			glog.Infof("Error processing GraphQL schema: %s.  "+
   319  				"Admin server is connected, but no GraphQL schema is being served.", err)
   320  			break
   321  		}
   322  
   323  		glog.Infof("Successfully loaded GraphQL schema.  Serving GraphQL API.")
   324  
   325  		as.schema = *sch
   326  		as.status = healthy
   327  		as.resetSchema(generatedSchema)
   328  
   329  		break
   330  	}
   331  }
   332  
   333  func checkAdminSchemaExists() error {
   334  	// We could query for existing schema and only alter if it's not there, but
   335  	// this has same effect.  We might eventually have to migrate old versions of the
   336  	// metadata here.
   337  	_, err := (&edgraph.Server{}).Alter(context.Background(),
   338  		&dgoapi.Operation{Schema: dgraphAdminSchema})
   339  	return err
   340  }
   341  
   342  // addConnectedAdminResolvers sets up the real resolvers
   343  func (as *adminServer) addConnectedAdminResolvers() {
   344  
   345  	qryRw := resolve.NewQueryRewriter()
   346  	addRw := resolve.NewAddRewriter()
   347  	updRw := resolve.NewUpdateRewriter()
   348  	qryExec := resolve.DgraphAsQueryExecutor()
   349  	mutExec := resolve.DgraphAsMutationExecutor()
   350  
   351  	as.fns.Qe = qryExec
   352  	as.fns.Me = mutExec
   353  
   354  	as.rf.WithQueryResolver("health",
   355  		func(q schema.Query) resolve.QueryResolver {
   356  			health := &healthResolver{
   357  				status: as.status,
   358  			}
   359  
   360  			return resolve.NewQueryResolver(
   361  				health,
   362  				health,
   363  				resolve.StdQueryCompletion())
   364  		}).
   365  		WithMutationResolver("updateGQLSchema",
   366  			func(m schema.Mutation) resolve.MutationResolver {
   367  				updResolver := &updateSchemaResolver{
   368  					admin:                as,
   369  					baseAddRewriter:      addRw,
   370  					baseMutationRewriter: updRw,
   371  					baseMutationExecutor: mutExec,
   372  				}
   373  
   374  				return resolve.NewMutationResolver(
   375  					updResolver,
   376  					updResolver,
   377  					updResolver,
   378  					resolve.StdMutationCompletion(m.Name()))
   379  			}).
   380  		WithQueryResolver("getGQLSchema",
   381  			func(q schema.Query) resolve.QueryResolver {
   382  				getResolver := &getSchemaResolver{
   383  					admin:        as,
   384  					baseRewriter: qryRw,
   385  					baseExecutor: qryExec,
   386  				}
   387  
   388  				return resolve.NewQueryResolver(
   389  					getResolver,
   390  					getResolver,
   391  					resolve.StdQueryCompletion())
   392  			})
   393  }
   394  
   395  func getCurrentGraphQLSchema(r *resolve.RequestResolver) (*gqlSchema, error) {
   396  	req := &schema.Request{
   397  		Query: `query { getGQLSchema { id schema } }`}
   398  	resp := r.Resolve(context.Background(), req)
   399  	if len(resp.Errors) > 0 || resp.Data.Len() == 0 {
   400  		return nil, resp.Errors
   401  	}
   402  
   403  	var result struct {
   404  		GetGQLSchema *gqlSchema
   405  	}
   406  
   407  	err := json.Unmarshal(resp.Data.Bytes(), &result)
   408  
   409  	return result.GetGQLSchema, err
   410  }
   411  
   412  func resolverFactoryWithErrorMsg(msg string) resolve.ResolverFactory {
   413  	errFunc := func(name string) error { return errors.Errorf(msg, name) }
   414  	qErr :=
   415  		resolve.QueryResolverFunc(func(ctx context.Context, query schema.Query) *resolve.Resolved {
   416  			return &resolve.Resolved{Err: errFunc(query.ResponseName())}
   417  		})
   418  
   419  	mErr := resolve.MutationResolverFunc(
   420  		func(ctx context.Context, mutation schema.Mutation) (*resolve.Resolved, bool) {
   421  			return &resolve.Resolved{Err: errFunc(mutation.ResponseName())}, false
   422  		})
   423  
   424  	return resolve.NewResolverFactory(qErr, mErr)
   425  }
   426  
   427  func (as *adminServer) resetSchema(gqlSchema schema.Schema) {
   428  
   429  	resolverFactory := resolverFactoryWithErrorMsg(errResolverNotFound).
   430  		WithConventionResolvers(gqlSchema, as.fns)
   431  	if as.withIntrospection {
   432  		resolverFactory.WithSchemaIntrospection()
   433  	}
   434  
   435  	as.gqlServer.ServeGQL(resolve.New(gqlSchema, resolverFactory))
   436  
   437  	as.status = healthy
   438  }