diff --git a/agent/health_endpoint.go b/agent/health_endpoint.go index 9c0aac2b62..e57b5f48b6 100644 --- a/agent/health_endpoint.go +++ b/agent/health_endpoint.go @@ -143,9 +143,21 @@ RETRY_ONCE: return out.HealthChecks, nil } +func (s *HTTPServer) HealthConnectServiceNodes(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + return s.healthServiceNodes(resp, req, true) +} + func (s *HTTPServer) HealthServiceNodes(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + return s.healthServiceNodes(resp, req, false) +} + +func (s *HTTPServer) healthServiceNodes(resp http.ResponseWriter, req *http.Request, connect bool) (interface{}, error) { + if req.Method != "GET" { + return nil, MethodNotAllowedError{req.Method, []string{"GET"}} + } + // Set default DC - args := structs.ServiceSpecificRequest{} + args := structs.ServiceSpecificRequest{Connect: connect} s.parseSource(req, &args.Source) args.NodeMetaFilters = s.parseMetaFilter(req) if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done { @@ -159,8 +171,14 @@ func (s *HTTPServer) HealthServiceNodes(resp http.ResponseWriter, req *http.Requ args.TagFilter = true } + // Determine the prefix + prefix := "/v1/health/service/" + if connect { + prefix = "/v1/health/connect/" + } + // Pull out the service name - args.ServiceName = strings.TrimPrefix(req.URL.Path, "/v1/health/service/") + args.ServiceName = strings.TrimPrefix(req.URL.Path, prefix) if args.ServiceName == "" { resp.WriteHeader(http.StatusBadRequest) fmt.Fprint(resp, "Missing service name") diff --git a/agent/health_endpoint_test.go b/agent/health_endpoint_test.go index 5d2ae1445e..688924df1b 100644 --- a/agent/health_endpoint_test.go +++ b/agent/health_endpoint_test.go @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/testutil/retry" "github.com/hashicorp/serf/coordinate" + "github.com/stretchr/testify/assert" ) func TestHealthChecksInState(t *testing.T) { @@ -770,6 +771,105 @@ func TestHealthServiceNodes_WanTranslation(t *testing.T) { } } +func TestHealthConnectServiceNodes(t *testing.T) { + t.Parallel() + + assert := assert.New(t) + a := NewTestAgent(t.Name(), "") + defer a.Shutdown() + + // Register + args := structs.TestRegisterRequestProxy(t) + var out struct{} + assert.Nil(a.RPC("Catalog.Register", args, &out)) + + // Request + req, _ := http.NewRequest("GET", fmt.Sprintf( + "/v1/health/connect/%s?dc=dc1", args.Service.ProxyDestination), nil) + resp := httptest.NewRecorder() + obj, err := a.srv.HealthConnectServiceNodes(resp, req) + assert.Nil(err) + assertIndex(t, resp) + + // Should be a non-nil empty list for checks + nodes := obj.(structs.CheckServiceNodes) + assert.Len(nodes, 1) + assert.Len(nodes[0].Checks, 0) +} + +func TestHealthConnectServiceNodes_PassingFilter(t *testing.T) { + t.Parallel() + + a := NewTestAgent(t.Name(), "") + defer a.Shutdown() + + // Register + args := structs.TestRegisterRequestProxy(t) + args.Check = &structs.HealthCheck{ + Node: args.Node, + Name: "check", + ServiceID: args.Service.Service, + Status: api.HealthCritical, + } + var out struct{} + assert.Nil(t, a.RPC("Catalog.Register", args, &out)) + + t.Run("bc_no_query_value", func(t *testing.T) { + assert := assert.New(t) + req, _ := http.NewRequest("GET", fmt.Sprintf( + "/v1/health/connect/%s?passing", args.Service.ProxyDestination), nil) + resp := httptest.NewRecorder() + obj, err := a.srv.HealthConnectServiceNodes(resp, req) + assert.Nil(err) + assertIndex(t, resp) + + // Should be 0 health check for consul + nodes := obj.(structs.CheckServiceNodes) + assert.Len(nodes, 0) + }) + + t.Run("passing_true", func(t *testing.T) { + assert := assert.New(t) + req, _ := http.NewRequest("GET", fmt.Sprintf( + "/v1/health/connect/%s?passing=true", args.Service.ProxyDestination), nil) + resp := httptest.NewRecorder() + obj, err := a.srv.HealthConnectServiceNodes(resp, req) + assert.Nil(err) + assertIndex(t, resp) + + // Should be 0 health check for consul + nodes := obj.(structs.CheckServiceNodes) + assert.Len(nodes, 0) + }) + + t.Run("passing_false", func(t *testing.T) { + assert := assert.New(t) + req, _ := http.NewRequest("GET", fmt.Sprintf( + "/v1/health/connect/%s?passing=false", args.Service.ProxyDestination), nil) + resp := httptest.NewRecorder() + obj, err := a.srv.HealthConnectServiceNodes(resp, req) + assert.Nil(err) + assertIndex(t, resp) + + // Should be 0 health check for consul + nodes := obj.(structs.CheckServiceNodes) + assert.Len(nodes, 1) + }) + + t.Run("passing_bad", func(t *testing.T) { + assert := assert.New(t) + req, _ := http.NewRequest("GET", fmt.Sprintf( + "/v1/health/connect/%s?passing=nope-nope", args.Service.ProxyDestination), nil) + resp := httptest.NewRecorder() + a.srv.HealthConnectServiceNodes(resp, req) + assert.Equal(400, resp.Code) + + body, err := ioutil.ReadAll(resp.Body) + assert.Nil(err) + assert.True(bytes.Contains(body, []byte("Invalid value for ?passing"))) + }) +} + func TestFilterNonPassing(t *testing.T) { t.Parallel() nodes := structs.CheckServiceNodes{ diff --git a/agent/http_oss.go b/agent/http_oss.go index 185c8c1e0c..2e2c9751a6 100644 --- a/agent/http_oss.go +++ b/agent/http_oss.go @@ -53,6 +53,7 @@ func init() { registerEndpoint("/v1/health/checks/", []string{"GET"}, (*HTTPServer).HealthServiceChecks) registerEndpoint("/v1/health/state/", []string{"GET"}, (*HTTPServer).HealthChecksInState) registerEndpoint("/v1/health/service/", []string{"GET"}, (*HTTPServer).HealthServiceNodes) + registerEndpoint("/v1/health/connect/", []string{"GET"}, (*HTTPServer).HealthConnectServiceNodes) registerEndpoint("/v1/internal/ui/nodes", []string{"GET"}, (*HTTPServer).UINodes) registerEndpoint("/v1/internal/ui/node/", []string{"GET"}, (*HTTPServer).UINodeInfo) registerEndpoint("/v1/internal/ui/services", []string{"GET"}, (*HTTPServer).UIServices)