From 144db7c2b843180c014afd5d3721440305e3b3fe Mon Sep 17 00:00:00 2001 From: Aman Mangal Date: Mon, 6 Jan 2025 16:18:28 +0530 Subject: [PATCH] add grpc API for creating and dropping namespace --- dgraphapi/cluster.go | 12 +++++ dgraphtest/paths.go | 7 +-- edgraph/access_ee.go | 8 ++++ edgraph/multi_tenancy.go | 2 +- edgraph/multi_tenancy_ee.go | 4 +- edgraph/namepsace_test.go | 95 +++++++++++++++++++++++++++++++++++++ edgraph/server.go | 92 +++++++++++++++++++++++++++++++++++ go.mod | 2 +- go.sum | 4 +- graphql/admin/namespace.go | 2 +- schema/schema.go | 15 +++++- 11 files changed, 230 insertions(+), 13 deletions(-) create mode 100644 edgraph/namepsace_test.go diff --git a/dgraphapi/cluster.go b/dgraphapi/cluster.go index 7422756a6e2..c8992062285 100644 --- a/dgraphapi/cluster.go +++ b/dgraphapi/cluster.go @@ -740,6 +740,18 @@ func (gc *GrpcClient) DropPredicate(pred string) error { return gc.Alter(ctx, &api.Operation{DropAttr: pred}) } +func (gc *GrpcClient) CreateNamespace(namespace string) error { + ctx, cancel := context.WithTimeout(context.Background(), requestTimeout) + defer cancel() + return gc.Dgraph.CreateNamespace(ctx, namespace) +} + +func (gc *GrpcClient) DropNamespace(namespace string) error { + ctx, cancel := context.WithTimeout(context.Background(), requestTimeout) + defer cancel() + return gc.Dgraph.DropNamespace(ctx, namespace) +} + // Mutate performs a given mutation in a txn func (gc *GrpcClient) Mutate(mu *api.Mutation) (*api.Response, error) { txn := gc.NewTxn() diff --git a/dgraphtest/paths.go b/dgraphtest/paths.go index d3b5db032fb..31996051ad6 100644 --- a/dgraphtest/paths.go +++ b/dgraphtest/paths.go @@ -19,6 +19,7 @@ package dgraphtest import ( "log" "os" + "path" "path/filepath" "runtime" "strings" @@ -45,12 +46,8 @@ func init() { basePath := strings.ReplaceAll(thisFilePath, "/paths.go", "") baseRepoDir = strings.ReplaceAll(basePath, "/dgraphtest", "") binariesPath = filepath.Join(basePath, "binaries") + repoDir = path.Join(os.TempDir(), "temp-dgraph-repo") - var err error - repoDir, err = os.MkdirTemp("", "dgraph-repo") - if err != nil { - panic(err) - } if err := ensureDgraphClone(); err != nil { panic(err) } diff --git a/edgraph/access_ee.go b/edgraph/access_ee.go index 97be2ef0de5..ce97a9fd7fb 100644 --- a/edgraph/access_ee.go +++ b/edgraph/access_ee.go @@ -150,6 +150,14 @@ func (s *Server) authenticateLogin(ctx context.Context, request *api.LoginReques return user, nil } + if request.NamespaceName != "" { + ns, err := getNamespaceID(ctx, request.NamespaceName) + if err != nil { + return nil, err + } + request.Namespace = ns + } + // In case of login, we can't extract namespace from JWT because we have not yet given JWT // to the user, so the login request should contain the namespace, which is then set to ctx. ctx = x.AttachNamespace(ctx, request.Namespace) diff --git a/edgraph/multi_tenancy.go b/edgraph/multi_tenancy.go index 40d638dafb6..7203cd7e397 100644 --- a/edgraph/multi_tenancy.go +++ b/edgraph/multi_tenancy.go @@ -27,7 +27,7 @@ type ResetPasswordInput struct { Namespace uint64 } -func (s *Server) CreateNamespace(ctx context.Context, passwd string) (uint64, error) { +func (s *Server) CreateNamespaceInternal(ctx context.Context, passwd string) (uint64, error) { return 0, nil } diff --git a/edgraph/multi_tenancy_ee.go b/edgraph/multi_tenancy_ee.go index 9a63d2470a9..cfdbcd5f81c 100644 --- a/edgraph/multi_tenancy_ee.go +++ b/edgraph/multi_tenancy_ee.go @@ -88,9 +88,9 @@ func (s *Server) ResetPassword(ctx context.Context, inp *ResetPasswordInput) err return nil } -// CreateNamespace creates a new namespace. Only guardian of galaxy is authorized to do so. +// CreateNamespaceInternal creates a new namespace. Only guardian of galaxy is authorized to do so. // Authorization is handled by middlewares. -func (s *Server) CreateNamespace(ctx context.Context, passwd string) (uint64, error) { +func (s *Server) CreateNamespaceInternal(ctx context.Context, passwd string) (uint64, error) { glog.V(2).Info("Got create namespace request.") num := &pb.Num{Val: 1, Type: pb.Num_NS_ID} diff --git a/edgraph/namepsace_test.go b/edgraph/namepsace_test.go new file mode 100644 index 00000000000..8176cf33a2d --- /dev/null +++ b/edgraph/namepsace_test.go @@ -0,0 +1,95 @@ +//go:build integration + +/* + * Copyright 2017-2023 Dgraph Labs, Inc. and Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package edgraph + +import ( + "context" + "testing" + + "github.com/dgraph-io/dgo/v240/protos/api" + "github.com/dgraph-io/dgraph/v24/dgraphapi" + "github.com/dgraph-io/dgraph/v24/dgraphtest" + "github.com/stretchr/testify/require" +) + +func TestCreateNamespace(t *testing.T) { + dc := dgraphtest.NewComposeCluster() + client, cleanup, err := dc.Client() + require.NoError(t, err) + defer cleanup() + + // Drop all data + client.DropAll() + + // Create two namespaces + require.NoError(t, client.LoginIntoNamespace(context.Background(), + dgraphapi.DefaultUser, dgraphapi.DefaultPassword, 0)) + t.Logf("Creating namespace ns1") + require.NoError(t, client.CreateNamespace("ns1")) + t.Logf("Creating namespace ns2") + require.NoError(t, client.CreateNamespace("ns2")) + + // namespace 1 + t.Logf("Logging into namespace ns1") + require.NoError(t, client.LoginIntoNamespaceWithName(context.Background(), + dgraphapi.DefaultUser, dgraphapi.DefaultPassword, "ns1")) + + t.Logf("Setting up schema in namespace ns1") + require.NoError(t, client.SetupSchema(`name: string @index(exact) .`)) + + t.Logf("Mutating data in namespace ns1") + _, err = client.Mutate(&api.Mutation{ + SetNquads: []byte(`_:a "Alice" .`), + CommitNow: true, + }) + require.NoError(t, err) + + t.Logf("Querying data in namespace ns1") + resp, err := client.Query(`{ q(func: has(name)) { name } }`) + require.NoError(t, err) + require.JSONEq(t, `{"q":[{"name":"Alice"}]}`, string(resp.GetJson())) + + // setup schema in namespace 2 + t.Logf("Logging into namespace ns2") + require.NoError(t, client.LoginIntoNamespaceWithName(context.Background(), + dgraphapi.DefaultUser, dgraphapi.DefaultPassword, "ns2")) + + t.Logf("Setting up schema in namespace ns2") + require.NoError(t, client.SetupSchema(`name: string @index(exact) .`)) + + t.Logf("Mutating data in namespace ns2") + client.LoginIntoNamespaceWithName(context.Background(), dgraphapi.DefaultUser, + dgraphapi.DefaultPassword, "ns2") + _, err = client.Mutate(&api.Mutation{ + SetNquads: []byte(`_:a "Bob" .`), + CommitNow: true, + }) + require.NoError(t, err) + + t.Logf("Querying data in namespace ns2") + require.NoError(t, client.LoginIntoNamespaceWithName(context.Background(), + dgraphapi.DefaultUser, dgraphapi.DefaultPassword, "ns2")) + resp, err = client.Query(`{ q(func: has(name)) { name } }`) + require.NoError(t, err) + require.JSONEq(t, `{"q":[{"name":"Bob"}]}`, string(resp.GetJson())) +} + +// A test where we are creating a lots of namespaces constantly and in parallel + +// What if I create the same namespace again? diff --git a/edgraph/server.go b/edgraph/server.go index 63a9a543932..36a8e15c2f2 100644 --- a/edgraph/server.go +++ b/edgraph/server.go @@ -1895,6 +1895,98 @@ func (s *Server) CheckVersion(ctx context.Context, c *api.Check) (v *api.Version return v, nil } +func (s *Server) CreateNamespace(ctx context.Context, in *api.CreateNamespaceRequest) ( + *api.CreateNamespaceResponse, error) { + + if err := AuthGuardianOfTheGalaxy(ctx); err != nil { + s := status.Convert(err) + return nil, status.Error(s.Code(), + "Non guardian of galaxy user cannot create namespace. "+s.Message()) + } + + if _, err := getNamespaceID(ctx, in.Name); err == nil { + return nil, errors.Errorf("namespace %q already exists", in.Name) + } else if !strings.Contains(err.Error(), "not found") { + return nil, err + } + + password := "password" + if len(in.Password) != 0 { + password = in.Password + } + + ns, err := (&Server{}).CreateNamespaceInternal(ctx, password) + if err != nil { + return nil, err + } + + // If we crash at this point, it is possible that namespaces is created + // but no entry has been added to dgraph.namespace predicate. This is alright + // because we have not let the user know that namespace has been created. + // The user would have to try again and another namespace then would be + // assigned to the provided name here. + + ictx := x.AttachNamespace(ctx, 0) + _, err = (&Server{}).QueryNoGrpc(ictx, &api.Request{ + Mutations: []*api.Mutation{{ + Set: []*api.NQuad{ + { + Subject: "_:ns", + Predicate: "dgraph.namespace.name", + ObjectValue: &api.Value{Val: &api.Value_StrVal{StrVal: in.Name}}, + }, + { + Subject: "_:ns", + Predicate: "dgraph.namespace.id", + ObjectValue: &api.Value{Val: &api.Value_IntVal{IntVal: int64(ns)}}, + }, + }, + }}, + CommitNow: true, + }) + if err != nil { + return nil, err + } + + return &api.CreateNamespaceResponse{Namespace: ns}, nil +} + +func (s *Server) DropNamespace(ctx context.Context, in *api.DropNamespaceRequest) ( + *api.DropNamespaceResponse, error) { + + ns, err := getNamespaceID(ctx, in.Name) + if err != nil { + return nil, err + } + + return &api.DropNamespaceResponse{}, (&Server{}).DeleteNamespace(ctx, ns) +} + +func getNamespaceID(ctx context.Context, namespaceName string) (uint64, error) { + ctx = x.AttachNamespace(ctx, 0) + + q := `{q(func: eq(dgraph.namespace.name, "` + namespaceName + `")) { dgraph.namespace.id }}` + resp, err := (&Server{}).doQuery(ctx, &Request{req: &api.Request{Query: q}, doAuth: NoAuthorize}) + if err != nil { + return 0, err + } + + var data struct { + Data []struct { + ID int64 `json:"dgraph.namespace.id"` + } `json:"q"` + } + if err := json.Unmarshal(resp.GetJson(), &data); err != nil { + return 0, err + } + + if len(data.Data) == 0 { + return 0, errors.Errorf("namespace %q not found", namespaceName) + } + + return uint64(data.Data[0].ID), nil +} + // ------------------------------------------------------------------------------------------------- // HELPER FUNCTIONS // ------------------------------------------------------------------------------------------------- diff --git a/go.mod b/go.mod index a6b0f9f6206..f428d885e51 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/Masterminds/semver/v3 v3.3.1 github.com/blevesearch/bleve/v2 v2.4.4 github.com/dgraph-io/badger/v4 v4.5.0 - github.com/dgraph-io/dgo/v240 v240.1.0 + github.com/dgraph-io/dgo/v240 v240.1.1-0.20250106104422-238f3f6096e5 github.com/dgraph-io/gqlgen v0.13.2 github.com/dgraph-io/gqlparser/v2 v2.2.2 github.com/dgraph-io/graphql-transport-ws v0.0.0-20210511143556-2cef522f1f15 diff --git a/go.sum b/go.sum index 795e6a4ca35..729e4511c74 100644 --- a/go.sum +++ b/go.sum @@ -136,8 +136,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgraph-io/badger/v4 v4.5.0 h1:TeJE3I1pIWLBjYhIYCA1+uxrjWEoJXImFBMEBVSm16g= github.com/dgraph-io/badger/v4 v4.5.0/go.mod h1:ysgYmIeG8dS/E8kwxT7xHyc7MkmwNYLRoYnFbr7387A= -github.com/dgraph-io/dgo/v240 v240.1.0 h1:xd8z9kEXDWOAblaLJ2HLg2tXD6ngMQwq3ehLUS7GKNg= -github.com/dgraph-io/dgo/v240 v240.1.0/go.mod h1:r8WASETKfodzKqThSAhhTNIzcEMychArKKlZXQufWuA= +github.com/dgraph-io/dgo/v240 v240.1.1-0.20250106104422-238f3f6096e5 h1:A8mnedEBi5QgtTgCWN4IMeMo9c6d6ubxamLXNfMsnfg= +github.com/dgraph-io/dgo/v240 v240.1.1-0.20250106104422-238f3f6096e5/go.mod h1:W5xagB6LrVixmmq+hZH1SsLr/6Z0BS5j39pJNmqprAo= github.com/dgraph-io/gqlgen v0.13.2 h1:TNhndk+eHKj5qE7BenKKSYdSIdOGhLqxR1rCiMso9KM= github.com/dgraph-io/gqlgen v0.13.2/go.mod h1:iCOrOv9lngN7KAo+jMgvUPVDlYHdf7qDwsTkQby2Sis= github.com/dgraph-io/gqlparser/v2 v2.1.1/go.mod h1:MYS4jppjyx8b9tuUtjV7jU1UFZK6P9fvO8TsIsQtRKU= diff --git a/graphql/admin/namespace.go b/graphql/admin/namespace.go index 7822bf239bd..2ad10feceef 100644 --- a/graphql/admin/namespace.go +++ b/graphql/admin/namespace.go @@ -47,7 +47,7 @@ func resolveAddNamespace(ctx context.Context, m schema.Mutation) (*resolve.Resol req.Password = "password" } var ns uint64 - if ns, err = (&edgraph.Server{}).CreateNamespace(ctx, req.Password); err != nil { + if ns, err = (&edgraph.Server{}).CreateNamespaceInternal(ctx, req.Password); err != nil { return resolve.EmptyResult(m, err), false } return resolve.DataResult( diff --git a/schema/schema.go b/schema/schema.go index 00c1291346c..1bfba22caa3 100644 --- a/schema/schema.go +++ b/schema/schema.go @@ -783,7 +783,20 @@ func initialSchemaInternal(namespace uint64, all bool) []*pb.SchemaUpdate { ValueType: pb.Posting_STRING, Directive: pb.SchemaUpdate_INDEX, Tokenizer: []string{"sha256"}, - }) + }, &pb.SchemaUpdate{ + Predicate: "dgraph.namespace.name", + ValueType: pb.Posting_STRING, + Directive: pb.SchemaUpdate_INDEX, + Tokenizer: []string{"exact"}, + Unique: true, + }, &pb.SchemaUpdate{ + Predicate: "dgraph.namespace.id", + ValueType: pb.Posting_INT, + Directive: pb.SchemaUpdate_INDEX, + Tokenizer: []string{"int"}, + Unique: true, + }, + ) if all || x.WorkerConfig.AclEnabled { // propose the schema update for acl predicates