Skip to content

Commit

Permalink
Add ListServices to controller public api (linkerd#1876)
Browse files Browse the repository at this point in the history
Add a barebones ListServices endpoint, in support of autocomplete for services.
As we develop service profiles, this endpoint could probably be used to describe
more aspects of services (like, if there were some way to check whether a
service profile was enabled or not).

Accessible from the web UI via http:https://localhost:8084/api/services
  • Loading branch information
Risha Mars committed Nov 27, 2018
1 parent 73836f0 commit f8583df
Show file tree
Hide file tree
Showing 11 changed files with 570 additions and 221 deletions.
6 changes: 6 additions & 0 deletions controller/api/public/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ func (c *grpcOverHttpClient) ListPods(ctx context.Context, req *pb.ListPodsReque
return &msg, err
}

func (c *grpcOverHttpClient) ListServices(ctx context.Context, req *pb.ListServicesRequest, _ ...grpc.CallOption) (*pb.ListServicesResponse, error) {
var msg pb.ListServicesResponse
err := c.apiRequest(ctx, "ListServices", req, &msg)
return &msg, err
}

func (c *grpcOverHttpClient) Tap(ctx context.Context, req *pb.TapRequest, _ ...grpc.CallOption) (pb.Api_TapClient, error) {
return nil, status.Error(codes.Unimplemented, "Tap is deprecated, use TapByResource")
}
Expand Down
19 changes: 19 additions & 0 deletions controller/api/public/grpc_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,3 +254,22 @@ func (s *grpcServer) shouldIgnore(pod *k8sV1.Pod) bool {
}
return false
}

func (s *grpcServer) ListServices(ctx context.Context, req *pb.ListServicesRequest) (*pb.ListServicesResponse, error) {
log.Debugf("ListServices request: %+v", req)

services, err := s.k8sAPI.GetServices(req.Namespace, "")
if err != nil {
return nil, err
}

svcs := make([]*pb.Service, 0)
for _, svc := range services {
svcs = append(svcs, &pb.Service{
Name: svc.GetName(),
Namespace: svc.GetNamespace(),
})
}

return &pb.ListServicesResponse{Services: svcs}, nil
}
94 changes: 94 additions & 0 deletions controller/api/public/grpc_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,26 @@ type listPodsExpected struct {
res pb.ListPodsResponse
}

type listServicesExpected struct {
err error
k8sRes []string
res pb.ListServicesResponse
}

// sort Pods in ListPodResponses for easier comparison
type ByPod []*pb.Pod

func (bp ByPod) Len() int { return len(bp) }
func (bp ByPod) Swap(i, j int) { bp[i], bp[j] = bp[j], bp[i] }
func (bp ByPod) Less(i, j int) bool { return bp[i].Name <= bp[j].Name }

// sort Services in ListServiceResponses for easier comparison
type ByService []*pb.Service

func (bs ByService) Len() int { return len(bs) }
func (bs ByService) Swap(i, j int) { bs[i], bs[j] = bs[j], bs[i] }
func (bs ByService) Less(i, j int) bool { return bs[i].Name <= bs[j].Name }

func listPodResponsesEqual(a pb.ListPodsResponse, b pb.ListPodsResponse) bool {
if len(a.Pods) != len(b.Pods) {
return false
Expand Down Expand Up @@ -174,3 +187,84 @@ spec:
}
})
}

func listServiceResponsesEqual(a pb.ListServicesResponse, b pb.ListServicesResponse) bool {
if len(a.Services) != len(b.Services) {
return false
}

sort.Sort(ByService(a.Services))
sort.Sort(ByService(b.Services))

for i := 0; i < len(a.Services); i++ {
aSvc := a.Services[i]
bSvc := b.Services[i]

if aSvc.Name != bSvc.Name || aSvc.Namespace != bSvc.Namespace {
return false
}
}

return true
}
func TestListServices(t *testing.T) {
t.Run("Successfully queryies for services", func(t *testing.T) {
expectations := []listServicesExpected{
listServicesExpected{
err: nil,
k8sRes: []string{`
apiVersion: v1
kind: Service
metadata:
name: service-foo
namespace: emojivoto
`, `
apiVersion: v1
kind: Service
metadata:
name: service-bar
namespace: default
`,
},
res: pb.ListServicesResponse{
Services: []*pb.Service{
&pb.Service{
Name: "service-foo",
Namespace: "emojivoto",
},
&pb.Service{
Name: "service-bar",
Namespace: "default",
},
},
},
},
}

for _, exp := range expectations {
k8sAPI, err := k8s.NewFakeAPI("", exp.k8sRes...)
if err != nil {
t.Fatalf("NewFakeAPI returned an error: %s", err)
}

fakeGrpcServer := newGrpcServer(
&MockProm{},
tap.NewTapClient(nil),
k8sAPI,
"linkerd",
[]string{},
)

k8sAPI.Sync(nil)

rsp, err := fakeGrpcServer.ListServices(context.TODO(), &pb.ListServicesRequest{})
if err != exp.err {
t.Fatalf("Expected error: %s, Got: %s", exp.err, err)
}

if !listServiceResponsesEqual(exp.res, *rsp) {
t.Fatalf("Expected: %+v, Got: %+v", &exp.res, rsp)
}
}
})
}
25 changes: 25 additions & 0 deletions controller/api/public/http_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ var (
topRoutesPath = fullUrlPathFor("TopRoutes")
versionPath = fullUrlPathFor("Version")
listPodsPath = fullUrlPathFor("ListPods")
listServicesPath = fullUrlPathFor("ListServices")
tapByResourcePath = fullUrlPathFor("TapByResource")
selfCheckPath = fullUrlPathFor("SelfCheck")
)
Expand Down Expand Up @@ -49,6 +50,8 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
h.handleVersion(w, req)
case listPodsPath:
h.handleListPods(w, req)
case listServicesPath:
h.handleListServices(w, req)
case tapByResourcePath:
h.handleTapByResource(w, req)
case selfCheckPath:
Expand Down Expand Up @@ -164,6 +167,28 @@ func (h *handler) handleListPods(w http.ResponseWriter, req *http.Request) {
}
}

func (h *handler) handleListServices(w http.ResponseWriter, req *http.Request) {
var protoRequest pb.ListServicesRequest

err := httpRequestToProto(req, &protoRequest)
if err != nil {
writeErrorToHttpResponse(w, err)
return
}

rsp, err := h.grpcServer.ListServices(req.Context(), &protoRequest)
if err != nil {
writeErrorToHttpResponse(w, err)
return
}

err = writeProtoToHttpResponse(w, rsp)
if err != nil {
writeErrorToHttpResponse(w, err)
return
}
}

func (h *handler) handleTapByResource(w http.ResponseWriter, req *http.Request) {
flushableWriter, err := newStreamingWriter(w)
if err != nil {
Expand Down
5 changes: 5 additions & 0 deletions controller/api/public/http_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ func (m *mockGrpcServer) ListPods(ctx context.Context, req *pb.ListPodsRequest)
return m.ResponseToReturn.(*pb.ListPodsResponse), m.ErrorToReturn
}

func (m *mockGrpcServer) ListServices(ctx context.Context, req *pb.ListServicesRequest) (*pb.ListServicesResponse, error) {
m.LastRequestReceived = req
return m.ResponseToReturn.(*pb.ListServicesResponse), m.ErrorToReturn
}

func (m *mockGrpcServer) SelfCheck(ctx context.Context, req *healcheckPb.SelfCheckRequest) (*healcheckPb.SelfCheckResponse, error) {
m.LastRequestReceived = req
return m.ResponseToReturn.(*healcheckPb.SelfCheckResponse), m.ErrorToReturn
Expand Down
5 changes: 5 additions & 0 deletions controller/api/public/test_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type MockApiClient struct {
ErrorToReturn error
VersionInfoToReturn *pb.VersionInfo
ListPodsResponseToReturn *pb.ListPodsResponse
ListServicesResponseToReturn *pb.ListServicesResponse
StatSummaryResponseToReturn *pb.StatSummaryResponse
TopRoutesResponseToReturn *pb.TopRoutesResponse
SelfCheckResponseToReturn *healthcheckPb.SelfCheckResponse
Expand All @@ -45,6 +46,10 @@ func (c *MockApiClient) ListPods(ctx context.Context, in *pb.ListPodsRequest, op
return c.ListPodsResponseToReturn, c.ErrorToReturn
}

func (c *MockApiClient) ListServices(ctx context.Context, in *pb.ListServicesRequest, opts ...grpc.CallOption) (*pb.ListServicesResponse, error) {
return c.ListServicesResponseToReturn, c.ErrorToReturn
}

func (c *MockApiClient) Tap(ctx context.Context, in *pb.TapRequest, opts ...grpc.CallOption) (pb.Api_TapClient, error) {
return c.Api_TapClientToReturn, c.ErrorToReturn
}
Expand Down
Loading

0 comments on commit f8583df

Please sign in to comment.