diff --git a/graphql/admin/admin.go b/graphql/admin/admin.go index 80c49fcc4e2..06a04cf8f4c 100644 --- a/graphql/admin/admin.go +++ b/graphql/admin/admin.go @@ -150,6 +150,11 @@ const ( removed: [Member] cid: String license: License + """ + Contains list of namespaces. Note that this is not stored in proto's MembershipState and + computed at the time of query. + """ + namespaces: [UInt64] } type ClusterGroup { diff --git a/graphql/admin/state.go b/graphql/admin/state.go index bb687c49fed..e16960a7581 100644 --- a/graphql/admin/state.go +++ b/graphql/admin/state.go @@ -15,16 +15,17 @@ import ( ) type membershipState struct { - Counter uint64 `json:"counter,omitempty"` - Groups []clusterGroup `json:"groups,omitempty"` - Zeros []*pb.Member `json:"zeros,omitempty"` - MaxUID uint64 `json:"maxUID,omitempty"` - MaxNsID uint64 `json:"maxNsID,omitempty"` - MaxTxnTs uint64 `json:"maxTxnTs,omitempty"` - MaxRaftId uint64 `json:"maxRaftId,omitempty"` - Removed []*pb.Member `json:"removed,omitempty"` - Cid string `json:"cid,omitempty"` - License *pb.License `json:"license,omitempty"` + Counter uint64 `json:"counter,omitempty"` + Groups []clusterGroup `json:"groups,omitempty"` + Zeros []*pb.Member `json:"zeros,omitempty"` + MaxUID uint64 `json:"maxUID,omitempty"` + MaxNsID uint64 `json:"maxNsID,omitempty"` + MaxTxnTs uint64 `json:"maxTxnTs,omitempty"` + MaxRaftId uint64 `json:"maxRaftId,omitempty"` + Removed []*pb.Member `json:"removed,omitempty"` + Cid string `json:"cid,omitempty"` + License *pb.License `json:"license,omitempty"` + Namespaces []uint64 `json:"namespaces,omitempty"` } type clusterGroup struct { @@ -78,6 +79,9 @@ func resolveState(ctx context.Context, q schema.Query) *resolve.Resolved { func convertToGraphQLResp(ms pb.MembershipState) membershipState { var state membershipState + // namespaces stores set of namespaces + namespaces := make(map[uint64]struct{}) + state.Counter = ms.Counter for k, v := range ms.Groups { var members = make([]*pb.Member, 0, len(v.Members)) @@ -85,8 +89,12 @@ func convertToGraphQLResp(ms pb.MembershipState) membershipState { members = append(members, v1) } var tablets = make([]*pb.Tablet, 0, len(v.Tablets)) - for _, v1 := range v.Tablets { + for name, v1 := range v.Tablets { tablets = append(tablets, v1) + val, err := x.ExtractNamespaceFromPredicate(name) + if err == nil { + namespaces[val] = struct{}{} + } } state.Groups = append(state.Groups, clusterGroup{ Id: k, @@ -108,5 +116,10 @@ func convertToGraphQLResp(ms pb.MembershipState) membershipState { state.Cid = ms.Cid state.License = ms.License + state.Namespaces = []uint64{} + for ns := range namespaces { + state.Namespaces = append(state.Namespaces, ns) + } + return state } diff --git a/graphql/e2e/multi_tenancy/schema_test.go b/graphql/e2e/multi_tenancy/multi_tenancy_test.go similarity index 88% rename from graphql/e2e/multi_tenancy/schema_test.go rename to graphql/e2e/multi_tenancy/multi_tenancy_test.go index 3e5ae886de0..a5ab60392df 100644 --- a/graphql/e2e/multi_tenancy/schema_test.go +++ b/graphql/e2e/multi_tenancy/multi_tenancy_test.go @@ -14,12 +14,13 @@ * limitations under the License. */ -package schema +package multi_tenancy import ( "encoding/json" "io/ioutil" "net/http" + "strconv" "strings" "testing" "time" @@ -211,7 +212,7 @@ func TestGraphQLResponse(t *testing.T) { require.Equal(t, schema, common.AssertGetGQLSchema(t, common.Alpha1HTTP, header).Schema) require.Equal(t, schema, common.AssertGetGQLSchema(t, common.Alpha1HTTP, header1).Schema) - graphqlHelper(t, ` + queryHelper(t, groupOneGraphQLServer, ` mutation { addAuthor(input:{name: "Alice"}) { author{ @@ -227,7 +228,7 @@ func TestGraphQLResponse(t *testing.T) { } }`) - graphqlHelper(t, query, header, + queryHelper(t, groupOneGraphQLServer, query, header, `{ "queryAuthor": [ { @@ -236,7 +237,7 @@ func TestGraphQLResponse(t *testing.T) { ] }`) - graphqlHelper(t, query, header1, + queryHelper(t, groupOneGraphQLServer, query, header1, `{ "queryAuthor": [] }`) @@ -309,7 +310,7 @@ func TestAuth(t *testing.T) { Header: "Authorization", }) header.Set(accessJwtHeader, testutil.GrootHttpLogin(groupOneAdminServer).AccessJwt) - graphqlHelper(t, addUserMutation, header, `{ + queryHelper(t, groupOneGraphQLServer, addUserMutation, header, `{ "addUser": { "user":[{ "username":"Alice" @@ -326,7 +327,7 @@ func TestAuth(t *testing.T) { }) header1.Set(accessJwtHeader, testutil.GrootHttpLoginNamespace(groupOneAdminServer, ns).AccessJwt) - graphqlHelper(t, addUserMutation, header1, `{ + queryHelper(t, groupOneGraphQLServer, addUserMutation, header1, `{ "addUser": { "user":[{ "username":"Bob" @@ -385,13 +386,13 @@ func TestCORS(t *testing.T) { common.DeleteNamespace(t, ns, header) } -func graphqlHelper(t *testing.T, query string, headers http.Header, +func queryHelper(t *testing.T, server, query string, headers http.Header, expectedResult string) { params := &common.GraphQLParams{ Query: query, Headers: headers, } - queryResult := params.ExecuteAsPost(t, groupOneGraphQLServer) + queryResult := params.ExecuteAsPost(t, server) common.RequireNoGQLErrors(t, queryResult) testutil.CompareJSON(t, expectedResult, string(queryResult.Data)) } @@ -430,3 +431,53 @@ func testCORS(t *testing.T, namespace uint64, reqOrigin, expectedAllowedOrigin, common.RequireNoGQLErrors(t, gqlRes) testutil.CompareJSON(t, `{"queryTestCORS":[]}`, string(gqlRes.Data)) } + +// TestNamespacesQueryField checks that namespaces field in state query of /admin endpoint is +// properly working. +func TestNamespacesQueryField(t *testing.T) { + header := http.Header{} + header.Set(accessJwtHeader, testutil.GrootHttpLogin(groupOneAdminServer).AccessJwt) + + namespaceQuery := + `query { + state { + namespaces + } + }` + + // Test namespaces query shows 0 as the only namespace. + queryHelper(t, groupOneAdminServer, namespaceQuery, header, + `{ + "state": { + "namespaces":[0] + } + }`) + + ns1 := common.CreateNamespace(t, header) + ns2 := common.CreateNamespace(t, header) + header1 := http.Header{} + header1.Set(accessJwtHeader, testutil.GrootHttpLoginNamespace(groupOneAdminServer, + ns1).AccessJwt) + + // Test namespaces query shows no namespace in case user is not guardian of galaxy. + queryHelper(t, groupOneAdminServer, namespaceQuery, header1, + `{ + "state": { + "namespaces":[] + } + }`) + + // Test namespaces query shows all 3 namespaces, 0,ns1,ns2 in case user is guardian of galaxy. + queryHelper(t, groupOneAdminServer, namespaceQuery, header, + `{ + "state": { + "namespaces":[0,`+ + strconv.FormatUint(ns1, 10)+`,`+ + strconv.FormatUint(ns2, 10)+`] + } + }`) + + // cleanup + common.DeleteNamespace(t, ns1, header) + common.DeleteNamespace(t, ns2, header) +} diff --git a/x/keys.go b/x/keys.go index 418cba0c492..50467ab0109 100644 --- a/x/keys.go +++ b/x/keys.go @@ -118,6 +118,19 @@ func FormatNsAttr(attr string) string { return strconv.FormatUint(ns, 10) + "-" + attr } +func ExtractNamespaceFromPredicate(predicate string) (uint64, error) { + splitString := strings.Split(predicate, "-") + if len(splitString) <= 1 { + return 0, errors.Errorf("predicate does not contain namespace name") + } + uintVal, err := strconv.ParseUint(splitString[0], 0, 64) + if err != nil { + return 0, errors.Wrapf(err, "while parsing %s as uint64", splitString[0]) + } + return uintVal, nil + +} + func writeAttr(buf []byte, attr string) []byte { AssertTrue(len(attr) < math.MaxUint16) binary.BigEndian.PutUint16(buf[:2], uint16(len(attr)))