From 659a47baff8556a8c5cf44376129fa7b130a6e65 Mon Sep 17 00:00:00 2001 From: Aman Mangal Date: Thu, 30 Jan 2025 14:46:04 +0530 Subject: [PATCH] add grpc API for creating/deleting/rename/listing namespaces --- dgraphapi/cluster.go | 12 ++ edgraph/multi_tenancy.go | 2 +- edgraph/multi_tenancy_ee.go | 4 +- edgraph/namepsace_test.go | 115 +++++++++++++++++++ edgraph/server.go | 212 ++++++++++++++++++++++++++++++++++++ go.mod | 2 +- go.sum | 4 +- graphql/admin/namespace.go | 2 +- 8 files changed, 346 insertions(+), 7 deletions(-) create mode 100644 edgraph/namepsace_test.go diff --git a/dgraphapi/cluster.go b/dgraphapi/cluster.go index 8d3678d1dff..8fdfc99defa 100644 --- a/dgraphapi/cluster.go +++ b/dgraphapi/cluster.go @@ -702,6 +702,18 @@ func (hc *HTTPClient) PostDqlQuery(query string) ([]byte, error) { return DoReq(req) } +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) +} + func (hc *HTTPClient) Mutate(mutation string, commitNow bool) ([]byte, error) { url := hc.dqlMutateUrl if commitNow { diff --git a/edgraph/multi_tenancy.go b/edgraph/multi_tenancy.go index 60d904b0839..781ea35c38a 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 05b77a15f31..1f4cb360813 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..f2e923efb69 --- /dev/null +++ b/edgraph/namepsace_test.go @@ -0,0 +1,115 @@ +//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/hypermodeinc/dgraph/v24/dgraphapi" + "github.com/hypermodeinc/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? + +// wrong auth + +func TestCreateSameNamespace(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 ns4") + require.NoError(t, client.CreateNamespace("ns4")) + t.Logf("Creating namespace ns4 again") + require.ErrorContains(t, client.CreateNamespace("ns4"), `namespace "ns4" already exists`) +} diff --git a/edgraph/server.go b/edgraph/server.go index 83d6db10a7c..655e3bbb45c 100644 --- a/edgraph/server.go +++ b/edgraph/server.go @@ -1894,6 +1894,218 @@ 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 := verifyNamespaceName(in.NsName); err != nil { + return nil, err + } + + if _, err := getNamespaceID(ctx, in.NsName); err == nil { + return nil, errors.Errorf("namespace %q already exists", in.NsName) + } 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. + + _, err = (&Server{}).QueryNoGrpc(ctx, &api.Request{ + Mutations: []*api.Mutation{{ + Set: []*api.NQuad{ + { + Subject: "_:ns", + Predicate: "dgraph.namespace.name", + ObjectValue: &api.Value{Val: &api.Value_StrVal{StrVal: in.NsName}}, + }, + { + Subject: "_:ns", + Predicate: "dgraph.namespace.id", + ObjectValue: &api.Value{Val: &api.Value_IntVal{IntVal: int64(ns)}}, + }, + }, + }}, + CommitNow: true, + }) + if err != nil { + return nil, err + } + + glog.Infof("Created namespace [%v] with id [%d]", in.NsName, ns) + return &api.CreateNamespaceResponse{NsId: ns}, nil +} + +func (s *Server) DropNamespace(ctx context.Context, in *api.DropNamespaceRequest) ( + *api.DropNamespaceResponse, error) { + + if err := AuthGuardianOfTheGalaxy(ctx); err != nil { + s := status.Convert(err) + return nil, status.Error(s.Code(), + "Non guardian of galaxy user cannot drop namespace. "+s.Message()) + } + + ns, err := deleteNamespaceID(ctx, in.NsName) + if err != nil { + return nil, err + } + + if ns != 0 { + if err := (&Server{}).DeleteNamespace(ctx, ns); err != nil { + return nil, err + } + } + + glog.Infof("Dropped namespace [%v] with id [%d]", in.NsName, ns) + return &api.DropNamespaceResponse{}, nil +} + +func (s *Server) RenameNamespace(ctx context.Context, in *api.RenameNamespaceRequest) ( + *api.RenameNamespaceResponse, error) { + + if err := AuthGuardianOfTheGalaxy(ctx); err != nil { + s := status.Convert(err) + return nil, status.Error(s.Code(), + "Non guardian of galaxy user cannot rename a namespace. "+s.Message()) + } + + if err := verifyNamespaceName(in.ToNs); err != nil { + return nil, err + } + + resp, err := (&Server{}).QueryNoGrpc(ctx, &api.Request{ + Query: `ns(func: eq(dgraph.namespace.name, "` + in.FromNs + `") {dgraph.namespace.id})`, + Mutations: []*api.Mutation{{ + Cond: `@if(gt(len(ns), 0)`, + Set: []*api.NQuad{ + { + Subject: "uid(ns)", + Predicate: "dgraph.namespace.name", + ObjectValue: &api.Value{Val: &api.Value_StrVal{StrVal: in.ToNs}}, + }, + }, + }}, + CommitNow: true, + }) + if err != nil { + return nil, err + } + + var data struct { + Data []struct { + ID int64 `json:"dgraph.namespace.id"` + } `json:"ns"` + } + if err := json.Unmarshal(resp.GetJson(), &data); err != nil { + return nil, err + } + + if len(data.Data) == 0 { + return nil, errors.Errorf("namespace [%v] not found", in.FromNs) + } + + glog.Infof("Renamed namespace [%v] to [%v]", in.FromNs, in.ToNs) + return &api.RenameNamespaceResponse{}, nil +} + +func (s *Server) ListNamespaces(ctx context.Context, in *api.ListNamespacesRequest) ( + *api.ListNamespacesResponse, error) { + + // TODO + + return nil, nil +} + +// verifyNamespaceName ensures that name only has alpha numeric characters as +// well as underscores and hyphens. It also ensures that the name is not empty. +func verifyNamespaceName(name string) error { + if name == "" { + return errors.Errorf("namespace name cannot be empty") + } + hasInvalidChars := strings.ContainsFunc(name, func(r rune) bool { + return !unicode.IsLetter(r) && !unicode.IsNumber(r) && r != '_' && r != '-' + }) + if hasInvalidChars { + return fmt.Errorf("namespace name [%v] has invalid characters", name) + } + if strings.HasPrefix(name, "_") || strings.HasPrefix(name, "-") { + return fmt.Errorf("namespace name [%v] cannot start with _ or -", name) + } + if strings.HasPrefix(name, "dgraph") { + return fmt.Errorf("namespace name [%v] cannot start with dgraph", name) + } + return nil +} + +func getNamespaceID(ctx context.Context, namespaceName string) (uint64, error) { + const q = `{q(func: eq(dgraph.namespace.name, "%v")) { dgraph.namespace.id }}` + + req := &api.Request{Query: fmt.Sprintf(q, namespaceName)} + resp, err := (&Server{}).doQuery(ctx, &Request{req: req, 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 +} + +func deleteNamespaceID(ctx context.Context, namespaceName string) (uint64, error) { + const q = `{q(func: eq(dgraph.namespace.name, "%v")) { dgraph.namespace.id }}` + + req := &api.Request{ + Query: fmt.Sprintf(q, namespaceName), + Mutations: []*api.Mutation{{DelNquads: []byte(`uid(q) * *`)}}, + } + resp, err := (&Server{}).doQuery(ctx, &Request{req: req, 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, nil + } + + return uint64(data.Data[0].ID), nil +} + // ------------------------------------------------------------------------------------------------- // HELPER FUNCTIONS // ------------------------------------------------------------------------------------------------- diff --git a/go.mod b/go.mod index f0a31ad2192..544ed1d4a15 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.1 - github.com/dgraph-io/dgo/v240 v240.1.0 + github.com/dgraph-io/dgo/v240 v240.1.1-0.20250127134951-d6ef79342736 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 3ad08393b2f..bf11c7e5c60 100644 --- a/go.sum +++ b/go.sum @@ -129,8 +129,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.1 h1:7DCIXrQjo1LKmM96YD+hLVJ2EEsyyoWxJfpdd56HLps= github.com/dgraph-io/badger/v4 v4.5.1/go.mod h1:qn3Be0j3TfV4kPbVoK0arXCD1/nr1ftth6sbL5jxdoA= -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.20250127134951-d6ef79342736 h1:9wT2hCsJhPrGtif6+UmKCH1pLOfo5IeTSBETcVChqXA= +github.com/dgraph-io/dgo/v240 v240.1.1-0.20250127134951-d6ef79342736/go.mod h1:PIec6A1kGSDzt9CPMu7evxLlQCpBCMJnmcwfXQUYmqs= 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 60fc16e228f..bfdd0b19b26 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(