Skip to content

Commit df6e319

Browse files
feat(GraphQL): Zero HTTP endpoints are now available at GraphQL admin (GRAPHQL-1118) (#6649)
This PR makes Zero HTTP endpoints available in GraphQL admin. It also adds a flag `--disable_admin_http` in Zero which can be used to disable the administrative HTTP endpoints in Zero. The following are considered as the administrative endpoints for Zero: * `/state` * `/removeNode` * `/moveTablet` * `/assign` * `/enterpriseLicense` RFC: https://discuss.dgraph.io/t/moving-zero-http-endpoints-to-alpha-graphql-admin/10725 (cherry picked from commit 07be3c9) # Conflicts: # dgraph/cmd/zero/run.go # protos/pb/pb.pb.go
1 parent 3f9effa commit df6e319

24 files changed

+2012
-535
lines changed

dgraph/cmd/zero/http.go

+23-44
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ func (st *state) assign(w http.ResponseWriter, r *http.Request) {
6565
return
6666
}
6767

68-
num := &pb.Num{Val: uint64(val)}
68+
num := &pb.Num{Val: val}
6969
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
7070
defer cancel()
7171

@@ -87,7 +87,7 @@ func (st *state) assign(w http.ResponseWriter, r *http.Request) {
8787
ids, err = st.zero.AssignIds(ctx, num)
8888
default:
8989
x.SetStatus(w, x.Error,
90-
fmt.Sprintf("Invalid what: [%s]. Must be one of uids or timestamps", what))
90+
fmt.Sprintf("Invalid what: [%s]. Must be one of: [uids, timestamps, nsids]", what))
9191
return
9292
}
9393
if err != nil {
@@ -124,7 +124,10 @@ func (st *state) removeNode(w http.ResponseWriter, r *http.Request) {
124124
return
125125
}
126126

127-
if err := st.zero.removeNode(context.Background(), nodeId, uint32(groupId)); err != nil {
127+
if _, err := st.zero.RemoveNode(
128+
context.Background(),
129+
&pb.RemoveNodeRequest{NodeId: nodeId, GroupId: uint32(groupId)},
130+
); err != nil {
128131
x.SetStatus(w, x.Error, err.Error())
129132
return
130133
}
@@ -154,8 +157,6 @@ func (st *state) moveTablet(w http.ResponseWriter, r *http.Request) {
154157
return
155158
}
156159

157-
tablet := r.URL.Query().Get("tablet")
158-
159160
namespace := r.URL.Query().Get("namespace")
160161
namespace = strings.TrimSpace(namespace)
161162
ns := x.GalaxyNamespace
@@ -168,7 +169,7 @@ func (st *state) moveTablet(w http.ResponseWriter, r *http.Request) {
168169
}
169170
}
170171

171-
tablet = x.NamespaceAttr(ns, tablet)
172+
tablet := r.URL.Query().Get("tablet")
172173
if len(tablet) == 0 {
173174
w.WriteHeader(http.StatusBadRequest)
174175
x.SetStatus(w, x.ErrorInvalidRequest, "tablet is a mandatory query parameter")
@@ -178,50 +179,28 @@ func (st *state) moveTablet(w http.ResponseWriter, r *http.Request) {
178179
groupId, ok := intFromQueryParam(w, r, "group")
179180
if !ok {
180181
w.WriteHeader(http.StatusBadRequest)
181-
x.SetStatus(w, x.ErrorInvalidRequest, fmt.Sprintf(
182-
"Query parameter 'group' should contain a valid integer."))
183-
return
184-
}
185-
dstGroup := uint32(groupId)
186-
knownGroups := st.zero.KnownGroups()
187-
var isKnown bool
188-
for _, grp := range knownGroups {
189-
if grp == dstGroup {
190-
isKnown = true
191-
break
192-
}
193-
}
194-
if !isKnown {
195-
w.WriteHeader(http.StatusBadRequest)
196-
x.SetStatus(w, x.ErrorInvalidRequest, fmt.Sprintf("Group: [%d] is not a known group.",
197-
dstGroup))
198-
return
199-
}
200-
201-
tab := st.zero.ServingTablet(tablet)
202-
if tab == nil {
203-
w.WriteHeader(http.StatusBadRequest)
204-
x.SetStatus(w, x.ErrorInvalidRequest, fmt.Sprintf("No tablet found for: %s", tablet))
205-
return
206-
}
207-
208-
srcGroup := tab.GroupId
209-
if srcGroup == dstGroup {
210-
w.WriteHeader(http.StatusInternalServerError)
211182
x.SetStatus(w, x.ErrorInvalidRequest,
212-
fmt.Sprintf("Tablet: [%s] is already being served by group: [%d]", tablet, srcGroup))
183+
"Query parameter 'group' should contain a valid integer.")
213184
return
214185
}
186+
dstGroup := uint32(groupId)
215187

216-
if err := st.zero.movePredicate(tablet, srcGroup, dstGroup); err != nil {
217-
glog.Errorf("While moving predicate %s from %d -> %d. Error: %v",
218-
tablet, srcGroup, dstGroup, err)
219-
w.WriteHeader(http.StatusInternalServerError)
220-
x.SetStatus(w, x.Error, err.Error())
188+
var resp *pb.Status
189+
var err error
190+
if resp, err = st.zero.MoveTablet(
191+
context.Background(),
192+
&pb.MoveTabletRequest{Namespace: ns, Tablet: tablet, DstGroup: dstGroup},
193+
); err != nil {
194+
if resp.GetMsg() == x.ErrorInvalidRequest {
195+
w.WriteHeader(http.StatusBadRequest)
196+
x.SetStatus(w, x.ErrorInvalidRequest, err.Error())
197+
} else {
198+
w.WriteHeader(http.StatusInternalServerError)
199+
x.SetStatus(w, x.Error, err.Error())
200+
}
221201
return
222202
}
223-
_, err := fmt.Fprintf(w, "Predicate: [%s] moved from group [%d] to [%d]",
224-
tablet, srcGroup, dstGroup)
203+
_, err = fmt.Fprint(w, resp.GetMsg())
225204
if err != nil {
226205
glog.Warningf("Error while writing response: %+v", err)
227206
}

dgraph/cmd/zero/license_ee.go

+2-3
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
package zero
1414

1515
import (
16-
"bytes"
1716
"context"
1817
"io/ioutil"
1918
"math"
@@ -139,7 +138,7 @@ func (st *state) applyEnterpriseLicense(w http.ResponseWriter, r *http.Request)
139138

140139
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
141140
defer cancel()
142-
if err := st.zero.applyLicense(ctx, bytes.NewReader(b)); err != nil {
141+
if _, err := st.zero.ApplyLicense(ctx, &pb.ApplyLicenseRequest{License: b}); err != nil {
143142
w.WriteHeader(http.StatusBadRequest)
144143
x.SetStatus(w, x.ErrorInvalidRequest, err.Error())
145144
return
@@ -157,7 +156,7 @@ func (s *Server) applyLicenseFile(path string) {
157156
}
158157
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
159158
defer cancel()
160-
if err = s.applyLicense(ctx, bytes.NewReader(content)); err != nil {
159+
if _, err = s.ApplyLicense(ctx, &pb.ApplyLicenseRequest{License: content}); err != nil {
161160
glog.Infof("Unable to apply license at %v due to error %v", path, err)
162161
}
163162
}

dgraph/cmd/zero/run.go

+10-5
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ instances to achieve high-availability.
101101
flag.StringP("wal", "w", "zw", "Directory storing WAL.")
102102
flag.Duration("rebalance_interval", 8*time.Minute, "Interval for trying a predicate move.")
103103
flag.String("enterprise_license", "", "Path to the enterprise license file.")
104+
flag.Bool("disable_admin_http", false,
105+
"Turn on/off the administrative endpoints exposed over Zero's HTTP port.")
104106

105107
flag.String("limit", worker.ZeroLimitsDefaults, z.NewSuperFlagHelp(worker.ZeroLimitsDefaults).
106108
Head("Limit options").
@@ -325,11 +327,14 @@ func run() {
325327
http.Handle("/", audit.AuditRequestHttp(baseMux))
326328

327329
baseMux.HandleFunc("/health", st.pingResponse)
328-
baseMux.HandleFunc("/state", st.getState)
329-
baseMux.HandleFunc("/removeNode", st.removeNode)
330-
baseMux.HandleFunc("/moveTablet", st.moveTablet)
331-
baseMux.HandleFunc("/assign", st.assign)
332-
baseMux.HandleFunc("/enterpriseLicense", st.applyEnterpriseLicense)
330+
// the following endpoints are disabled only if the flag is explicitly set to true
331+
if !Zero.Conf.GetBool("disable_admin_http") {
332+
baseMux.HandleFunc("/state", st.getState)
333+
baseMux.HandleFunc("/removeNode", st.removeNode)
334+
baseMux.HandleFunc("/moveTablet", st.moveTablet)
335+
baseMux.HandleFunc("/assign", st.assign)
336+
baseMux.HandleFunc("/enterpriseLicense", st.applyEnterpriseLicense)
337+
}
333338
baseMux.HandleFunc("/debug/jemalloc", x.JemallocHandler)
334339
zpages.Handle(baseMux, "/debug/z")
335340

dgraph/cmd/zero/tablet.go

+46
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,52 @@ func (s *Server) rebalanceTablets() {
7272
}
7373
}
7474

75+
// MoveTablet can be used to move a tablet to a specific group.
76+
// It takes in tablet and destination group as argument.
77+
// It returns a *pb.Status to be used by the `/moveTablet` HTTP handler in Zero.
78+
func (s *Server) MoveTablet(ctx context.Context, req *pb.MoveTabletRequest) (*pb.Status, error) {
79+
if !s.Node.AmLeader() {
80+
return &pb.Status{Code: 1, Msg: x.Error}, errNotLeader
81+
}
82+
83+
knownGroups := s.KnownGroups()
84+
var isKnown bool
85+
for _, grp := range knownGroups {
86+
if grp == req.DstGroup {
87+
isKnown = true
88+
break
89+
}
90+
}
91+
if !isKnown {
92+
return &pb.Status{Code: 1, Msg: x.ErrorInvalidRequest},
93+
fmt.Errorf("Group: [%d] is not a known group.", req.DstGroup)
94+
}
95+
96+
tablet := x.NamespaceAttr(req.Namespace, req.Tablet)
97+
tab := s.ServingTablet(tablet)
98+
if tab == nil {
99+
return &pb.Status{Code: 1, Msg: x.ErrorInvalidRequest},
100+
fmt.Errorf("namespace: %d. No tablet found for: %s", req.Namespace, req.Tablet)
101+
}
102+
103+
srcGroup := tab.GroupId
104+
if srcGroup == req.DstGroup {
105+
return &pb.Status{Code: 1, Msg: x.ErrorInvalidRequest},
106+
fmt.Errorf("namespace: %d. Tablet: [%s] is already being served by group: [%d]",
107+
req.Namespace, req.Tablet, srcGroup)
108+
}
109+
110+
if err := s.movePredicate(tablet, srcGroup, req.DstGroup); err != nil {
111+
glog.Errorf("namespace: %d. While moving predicate %s from %d -> %d. Error: %v",
112+
req.Namespace, req.Tablet, srcGroup, req.DstGroup, err)
113+
return &pb.Status{Code: 1, Msg: x.Error}, err
114+
}
115+
116+
return &pb.Status{Code: 0, Msg: fmt.Sprintf("namespace: %d. "+
117+
"Predicate: [%s] moved from group [%d] to [%d]", req.Namespace, req.Tablet, srcGroup,
118+
req.DstGroup)}, nil
119+
}
120+
75121
// movePredicate is the main entry point for move predicate logic. This Zero must remain the leader
76122
// for the entire duration of predicate move. If this Zero stops being the leader, the final
77123
// proposal of reassigning the tablet to the destination would fail automatically.

dgraph/cmd/zero/zero.go

+27-19
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@
1717
package zero
1818

1919
import (
20+
"bytes"
2021
"context"
2122
"crypto/tls"
22-
"io"
2323
"math"
2424
"strings"
2525
"sync"
@@ -405,26 +405,32 @@ func (s *Server) createProposals(dst *pb.Group) ([]*pb.ZeroProposal, error) {
405405
return res, nil
406406
}
407407

408-
// removeNode removes the given node from the given group.
408+
// RemoveNode removes the given node from the given group.
409409
// It's the user's responsibility to ensure that node doesn't come back again
410410
// before calling the api.
411-
func (s *Server) removeNode(ctx context.Context, nodeId uint64, groupId uint32) error {
412-
if groupId == 0 {
413-
return s.Node.ProposePeerRemoval(ctx, nodeId)
411+
func (s *Server) RemoveNode(ctx context.Context, req *pb.RemoveNodeRequest) (*pb.Status, error) {
412+
if req.GroupId == 0 {
413+
return nil, s.Node.ProposePeerRemoval(ctx, req.NodeId)
414414
}
415415
zp := &pb.ZeroProposal{}
416-
zp.Member = &pb.Member{Id: nodeId, GroupId: groupId, AmDead: true}
417-
if _, ok := s.state.Groups[groupId]; !ok {
418-
return errors.Errorf("No group with groupId %d found", groupId)
416+
zp.Member = &pb.Member{Id: req.NodeId, GroupId: req.GroupId, AmDead: true}
417+
if _, ok := s.state.Groups[req.GroupId]; !ok {
418+
return nil, errors.Errorf("No group with groupId %d found", req.GroupId)
419419
}
420-
if _, ok := s.state.Groups[groupId].Members[nodeId]; !ok {
421-
return errors.Errorf("No node with nodeId %d found in group %d", nodeId, groupId)
420+
if _, ok := s.state.Groups[req.GroupId].Members[req.NodeId]; !ok {
421+
return nil, errors.Errorf("No node with nodeId %d found in group %d", req.NodeId,
422+
req.GroupId)
422423
}
423-
if len(s.state.Groups[groupId].Members) == 1 && len(s.state.Groups[groupId].Tablets) > 0 {
424-
return errors.Errorf("Move all tablets from group %d before removing the last node", groupId)
424+
if len(s.state.Groups[req.GroupId].Members) == 1 && len(s.state.Groups[req.GroupId].
425+
Tablets) > 0 {
426+
return nil, errors.Errorf("Move all tablets from group %d before removing the last node",
427+
req.GroupId)
428+
}
429+
if err := s.Node.proposeAndWait(ctx, zp); err != nil {
430+
return nil, err
425431
}
426432

427-
return s.Node.proposeAndWait(ctx, zp)
433+
return &pb.Status{}, nil
428434
}
429435

430436
// Connect is used by Alpha nodes to connect the very first time with group zero.
@@ -805,19 +811,21 @@ func (s *Server) latestMembershipState(ctx context.Context) (*pb.MembershipState
805811
return ms, nil
806812
}
807813

808-
func (s *Server) applyLicense(ctx context.Context, signedData io.Reader) error {
814+
func (s *Server) ApplyLicense(ctx context.Context, req *pb.ApplyLicenseRequest) (*pb.Status,
815+
error) {
809816
var l license
817+
signedData := bytes.NewReader(req.License)
810818
if err := verifySignature(signedData, strings.NewReader(publicKey), &l); err != nil {
811-
return errors.Wrapf(err, "while extracting enterprise details from the license")
819+
return nil, errors.Wrapf(err, "while extracting enterprise details from the license")
812820
}
813821

814822
numNodes := len(s.state.GetZeros())
815823
for _, group := range s.state.GetGroups() {
816824
numNodes += len(group.GetMembers())
817825
}
818826
if uint64(numNodes) > l.MaxNodes {
819-
return errors.Errorf("Your license only allows [%v] (Alpha + Zero) nodes. You have: [%v].",
820-
l.MaxNodes, numNodes)
827+
return nil, errors.Errorf("Your license only allows [%v] (Alpha + Zero) nodes. "+
828+
"You have: [%v].", l.MaxNodes, numNodes)
821829
}
822830

823831
proposal := &pb.ZeroProposal{
@@ -830,8 +838,8 @@ func (s *Server) applyLicense(ctx context.Context, signedData io.Reader) error {
830838

831839
err := s.Node.proposeAndWait(ctx, proposal)
832840
if err != nil {
833-
return errors.Wrapf(err, "while proposing enterprise license state to cluster")
841+
return nil, errors.Wrapf(err, "while proposing enterprise license state to cluster")
834842
}
835843
glog.Infof("Enterprise license proposed to the cluster %+v", proposal)
836-
return nil
844+
return &pb.Status{}, nil
837845
}

dgraph/cmd/zero/zero_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ func TestRemoveNode(t *testing.T) {
3030
Groups: map[uint32]*pb.Group{1: {Members: map[uint64]*pb.Member{}}},
3131
},
3232
}
33-
err := server.removeNode(context.TODO(), 3, 1)
33+
_, err := server.RemoveNode(context.TODO(), &pb.RemoveNodeRequest{NodeId: 3, GroupId: 1})
3434
require.Error(t, err)
35-
err = server.removeNode(context.TODO(), 1, 2)
35+
_, err = server.RemoveNode(context.TODO(), &pb.RemoveNodeRequest{NodeId: 1, GroupId: 2})
3636
require.Error(t, err)
3737
}

ee/acl/acl_test.go

+64
Original file line numberDiff line numberDiff line change
@@ -2736,6 +2736,70 @@ func TestGuardianOnlyAccessForAdminEndpoints(t *testing.T) {
27362736
guardianErr: "The path \"\" does not exist or it is inaccessible.",
27372737
guardianData: `{"restore": {"code": "Failure"}}`,
27382738
},
2739+
{
2740+
name: "removeNode has guardian auth",
2741+
query: `
2742+
mutation {
2743+
removeNode(input: {nodeId: 1, groupId: 2147483640}) {
2744+
response {
2745+
code
2746+
}
2747+
}
2748+
}`,
2749+
queryName: "removeNode",
2750+
testGuardianAccess: true,
2751+
guardianErr: "No group with groupId 2147483640 found",
2752+
guardianData: `{"removeNode": null}`,
2753+
},
2754+
{
2755+
name: "moveTablet has guardian auth",
2756+
query: `
2757+
mutation {
2758+
moveTablet(input: {tablet: "non_existent_pred", groupId: 2147483640}) {
2759+
response {
2760+
code
2761+
message
2762+
}
2763+
}
2764+
}`,
2765+
queryName: "moveTablet",
2766+
testGuardianAccess: true,
2767+
guardianErr: "Group: [2147483640] is not a known group.",
2768+
guardianData: `{"moveTablet": null}`,
2769+
},
2770+
{
2771+
name: "assign has guardian auth",
2772+
query: `
2773+
mutation {
2774+
assign(input: {what: UID, num: 0}) {
2775+
response {
2776+
startId
2777+
endId
2778+
readOnly
2779+
}
2780+
}
2781+
}`,
2782+
queryName: "assign",
2783+
testGuardianAccess: true,
2784+
guardianErr: "Nothing to be leased",
2785+
guardianData: `{"assign": null}`,
2786+
},
2787+
{
2788+
name: "enterpriseLicense has guardian auth",
2789+
query: `
2790+
mutation {
2791+
enterpriseLicense(input: {license: ""}) {
2792+
response {
2793+
code
2794+
}
2795+
}
2796+
}`,
2797+
queryName: "enterpriseLicense",
2798+
testGuardianAccess: true,
2799+
guardianErr: "while extracting enterprise details from the license: while decoding" +
2800+
" license file: EOF",
2801+
guardianData: `{"enterpriseLicense": null}`,
2802+
},
27392803
{
27402804
name: "getGQLSchema has guardian auth",
27412805
query: `

0 commit comments

Comments
 (0)