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 }