Skip to content

Commit

Permalink
add grpc API for creating/deleting/rename/listing namespaces
Browse files Browse the repository at this point in the history
  • Loading branch information
mangalaman93 committed Jan 31, 2025
1 parent 2aca89a commit 659a47b
Show file tree
Hide file tree
Showing 8 changed files with 346 additions and 7 deletions.
12 changes: 12 additions & 0 deletions dgraphapi/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion edgraph/multi_tenancy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
4 changes: 2 additions & 2 deletions edgraph/multi_tenancy_ee.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
115 changes: 115 additions & 0 deletions edgraph/namepsace_test.go
Original file line number Diff line number Diff line change
@@ -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"

Check failure on line 26 in edgraph/namepsace_test.go

View check run for this annotation

Trunk.io / Trunk Check

golangci-lint(typecheck)

[new] could not import github.com/hypermodeinc/dgraph/v24/dgraphapi (-: # github.com/hypermodeinc/dgraph/v24/dgraphapi dgraphapi/cluster.go:708:19: gc.Dgraph.CreateNamespace undefined (type *dgo.Dgraph has no field or method CreateNamespace) dgraphapi/cluster.go:714:19: gc.Dgraph.DropNamespace undefined (type *dgo.Dgraph has no field or method DropNamespace))
"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 <name> "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 <name> "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`)
}
212 changes: 212 additions & 0 deletions edgraph/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
// -------------------------------------------------------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
Loading

0 comments on commit 659a47b

Please sign in to comment.