Move ingress param to a new endpoint (#8081)

In discussion with team, it was pointed out that query parameters tend
to be filter mechanism, and that semantically the "/v1/health/connect"
endpoint should return "all healthy connect-enabled endpoints (e.g.
could be side car proxies or native instances) for this service so I can
connect with mTLS".

That does not fit an ingress gateway, so we remove the query parameter
and add a new endpoint "/v1/health/ingress" that semantically means
"all the healthy ingress gateway instances that I can connect to
to access this connect-enabled service without mTLS"
This commit is contained in:
Chris Piraino 2020-06-10 13:07:15 -05:00 committed by GitHub
parent 64ba8177e0
commit cba863af84
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 60 additions and 68 deletions

View File

@ -12,6 +12,12 @@ import (
"github.com/hashicorp/consul/api" "github.com/hashicorp/consul/api"
) )
const (
serviceHealth = "service"
connectHealth = "connect"
ingressHealth = "ingress"
)
func (s *HTTPServer) HealthChecksInState(resp http.ResponseWriter, req *http.Request) (interface{}, error) { func (s *HTTPServer) HealthChecksInState(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
// Set default DC // Set default DC
args := structs.ChecksInStateRequest{} args := structs.ChecksInStateRequest{}
@ -154,15 +160,26 @@ RETRY_ONCE:
return out.HealthChecks, nil return out.HealthChecks, nil
} }
// HealthIngressServiceNodes should return "all the healthy ingress gateway instances
// that I can use to access this connect-enabled service without mTLS".
func (s *HTTPServer) HealthIngressServiceNodes(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
return s.healthServiceNodes(resp, req, ingressHealth)
}
// HealthConnectServiceNodes should return "all healthy connect-enabled
// endpoints (e.g. could be side car proxies or native instances) for this
// service so I can connect with mTLS".
func (s *HTTPServer) HealthConnectServiceNodes(resp http.ResponseWriter, req *http.Request) (interface{}, error) { func (s *HTTPServer) HealthConnectServiceNodes(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
return s.healthServiceNodes(resp, req, true) return s.healthServiceNodes(resp, req, connectHealth)
} }
// HealthServiceNodes should return "all the healthy instances of this service
// registered so I can connect directly to them".
func (s *HTTPServer) HealthServiceNodes(resp http.ResponseWriter, req *http.Request) (interface{}, error) { func (s *HTTPServer) HealthServiceNodes(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
return s.healthServiceNodes(resp, req, false) return s.healthServiceNodes(resp, req, serviceHealth)
} }
func (s *HTTPServer) healthServiceNodes(resp http.ResponseWriter, req *http.Request, connect bool) (interface{}, error) { func (s *HTTPServer) healthServiceNodes(resp http.ResponseWriter, req *http.Request, healthType string) (interface{}, error) {
// Set default DC // Set default DC
args := structs.ServiceSpecificRequest{} args := structs.ServiceSpecificRequest{}
if err := s.parseEntMetaNoWildcard(req, &args.EnterpriseMeta); err != nil { if err := s.parseEntMetaNoWildcard(req, &args.EnterpriseMeta); err != nil {
@ -182,23 +199,17 @@ func (s *HTTPServer) healthServiceNodes(resp http.ResponseWriter, req *http.Requ
} }
// Determine the prefix // Determine the prefix
prefix := "/v1/health/service/" var prefix string
if connect { switch healthType {
case connectHealth:
prefix = "/v1/health/connect/" prefix = "/v1/health/connect/"
args.Connect = true
// Check for ingress request only when requesting connect services case ingressHealth:
ingress, err := getBoolQueryParam(params, "ingress") prefix = "/v1/health/ingress/"
if err != nil { args.Ingress = true
resp.WriteHeader(http.StatusBadRequest) default:
fmt.Fprint(resp, "Invalid value for ?ingress") // serviceHealth is the default type
return nil, nil prefix = "/v1/health/service/"
}
if ingress {
args.Ingress = true
} else {
args.Connect = true
}
} }
// Pull out the service name // Pull out the service name

View File

@ -1139,7 +1139,7 @@ func TestHealthConnectServiceNodes(t *testing.T) {
assert.Len(nodes[0].Checks, 0) assert.Len(nodes[0].Checks, 0)
} }
func TestHealthConnectServiceNodes_Ingress(t *testing.T) { func TestHealthIngressServiceNodes(t *testing.T) {
t.Parallel() t.Parallel()
a := NewTestAgent(t, "") a := NewTestAgent(t, "")
@ -1179,12 +1179,12 @@ func TestHealthConnectServiceNodes_Ingress(t *testing.T) {
require.Nil(t, a.RPC("ConfigEntry.Apply", req, &outB)) require.Nil(t, a.RPC("ConfigEntry.Apply", req, &outB))
require.True(t, outB) require.True(t, outB)
t.Run("no_query_value", func(t *testing.T) { t.Run("associated service", func(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
req, _ := http.NewRequest("GET", fmt.Sprintf( req, _ := http.NewRequest("GET", fmt.Sprintf(
"/v1/health/connect/%s?ingress", args.Service.Service), nil) "/v1/health/ingress/%s", args.Service.Service), nil)
resp := httptest.NewRecorder() resp := httptest.NewRecorder()
obj, err := a.srv.HealthConnectServiceNodes(resp, req) obj, err := a.srv.HealthIngressServiceNodes(resp, req)
assert.Nil(err) assert.Nil(err)
assertIndex(t, resp) assertIndex(t, resp)
@ -1195,47 +1195,18 @@ func TestHealthConnectServiceNodes_Ingress(t *testing.T) {
require.Equal(t, gatewayArgs.Service.Proxy, nodes[0].Service.Proxy) require.Equal(t, gatewayArgs.Service.Proxy, nodes[0].Service.Proxy)
}) })
t.Run("true_value", func(t *testing.T) { t.Run("non-associated service", func(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
req, _ := http.NewRequest("GET", fmt.Sprintf( req, _ := http.NewRequest("GET",
"/v1/health/connect/%s?ingress=true", args.Service.Service), nil) "/v1/health/connect/notexist", nil)
resp := httptest.NewRecorder() resp := httptest.NewRecorder()
obj, err := a.srv.HealthConnectServiceNodes(resp, req) obj, err := a.srv.HealthIngressServiceNodes(resp, req)
assert.Nil(err)
assertIndex(t, resp)
nodes := obj.(structs.CheckServiceNodes)
require.Len(t, nodes, 1)
require.Equal(t, structs.ServiceKindIngressGateway, nodes[0].Service.Kind)
require.Equal(t, gatewayArgs.Service.Address, nodes[0].Service.Address)
require.Equal(t, gatewayArgs.Service.Proxy, nodes[0].Service.Proxy)
})
t.Run("false_value", func(t *testing.T) {
assert := assert.New(t)
req, _ := http.NewRequest("GET", fmt.Sprintf(
"/v1/health/connect/%s?ingress=false", args.Service.Service), nil)
resp := httptest.NewRecorder()
obj, err := a.srv.HealthConnectServiceNodes(resp, req)
assert.Nil(err) assert.Nil(err)
assertIndex(t, resp) assertIndex(t, resp)
nodes := obj.(structs.CheckServiceNodes) nodes := obj.(structs.CheckServiceNodes)
require.Len(t, nodes, 0) require.Len(t, nodes, 0)
}) })
t.Run("invalid_value", func(t *testing.T) {
assert := assert.New(t)
req, _ := http.NewRequest("GET", fmt.Sprintf(
"/v1/health/connect/%s?ingress=notabool", args.Service.Service), nil)
resp := httptest.NewRecorder()
_, err := 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 ?ingress")))
})
} }
func TestHealthConnectServiceNodes_Filter(t *testing.T) { func TestHealthConnectServiceNodes_Filter(t *testing.T) {

View File

@ -91,6 +91,7 @@ func init() {
registerEndpoint("/v1/health/state/", []string{"GET"}, (*HTTPServer).HealthChecksInState) registerEndpoint("/v1/health/state/", []string{"GET"}, (*HTTPServer).HealthChecksInState)
registerEndpoint("/v1/health/service/", []string{"GET"}, (*HTTPServer).HealthServiceNodes) registerEndpoint("/v1/health/service/", []string{"GET"}, (*HTTPServer).HealthServiceNodes)
registerEndpoint("/v1/health/connect/", []string{"GET"}, (*HTTPServer).HealthConnectServiceNodes) registerEndpoint("/v1/health/connect/", []string{"GET"}, (*HTTPServer).HealthConnectServiceNodes)
registerEndpoint("/v1/health/ingress/", []string{"GET"}, (*HTTPServer).HealthIngressServiceNodes)
registerEndpoint("/v1/internal/ui/nodes", []string{"GET"}, (*HTTPServer).UINodes) registerEndpoint("/v1/internal/ui/nodes", []string{"GET"}, (*HTTPServer).UINodes)
registerEndpoint("/v1/internal/ui/node/", []string{"GET"}, (*HTTPServer).UINodeInfo) registerEndpoint("/v1/internal/ui/node/", []string{"GET"}, (*HTTPServer).UINodeInfo)
registerEndpoint("/v1/internal/ui/services", []string{"GET"}, (*HTTPServer).UIServices) registerEndpoint("/v1/internal/ui/services", []string{"GET"}, (*HTTPServer).UIServices)

View File

@ -17,6 +17,12 @@ const (
HealthMaint = "maintenance" HealthMaint = "maintenance"
) )
const (
serviceHealth = "service"
connectHealth = "connect"
ingressHealth = "ingress"
)
const ( const (
// NodeMaint is the special key set by a node in maintenance mode. // NodeMaint is the special key set by a node in maintenance mode.
NodeMaint = "_node_maintenance" NodeMaint = "_node_maintenance"
@ -269,11 +275,11 @@ func (h *Health) Service(service, tag string, passingOnly bool, q *QueryOptions)
if tag != "" { if tag != "" {
tags = []string{tag} tags = []string{tag}
} }
return h.service(service, tags, passingOnly, q, false, false) return h.service(service, tags, passingOnly, q, serviceHealth)
} }
func (h *Health) ServiceMultipleTags(service string, tags []string, passingOnly bool, q *QueryOptions) ([]*ServiceEntry, *QueryMeta, error) { func (h *Health) ServiceMultipleTags(service string, tags []string, passingOnly bool, q *QueryOptions) ([]*ServiceEntry, *QueryMeta, error) {
return h.service(service, tags, passingOnly, q, false, false) return h.service(service, tags, passingOnly, q, serviceHealth)
} }
// Connect is equivalent to Service except that it will only return services // Connect is equivalent to Service except that it will only return services
@ -286,25 +292,31 @@ func (h *Health) Connect(service, tag string, passingOnly bool, q *QueryOptions)
if tag != "" { if tag != "" {
tags = []string{tag} tags = []string{tag}
} }
return h.service(service, tags, passingOnly, q, true, false) return h.service(service, tags, passingOnly, q, connectHealth)
} }
func (h *Health) ConnectMultipleTags(service string, tags []string, passingOnly bool, q *QueryOptions) ([]*ServiceEntry, *QueryMeta, error) { func (h *Health) ConnectMultipleTags(service string, tags []string, passingOnly bool, q *QueryOptions) ([]*ServiceEntry, *QueryMeta, error) {
return h.service(service, tags, passingOnly, q, true, false) return h.service(service, tags, passingOnly, q, connectHealth)
} }
// Ingress is equivalent to Connect except that it will only return associated // Ingress is equivalent to Connect except that it will only return associated
// ingress gateways for the requested service. // ingress gateways for the requested service.
func (h *Health) Ingress(service string, passingOnly bool, q *QueryOptions) ([]*ServiceEntry, *QueryMeta, error) { func (h *Health) Ingress(service string, passingOnly bool, q *QueryOptions) ([]*ServiceEntry, *QueryMeta, error) {
var tags []string var tags []string
return h.service(service, tags, passingOnly, q, false, true) return h.service(service, tags, passingOnly, q, ingressHealth)
} }
func (h *Health) service(service string, tags []string, passingOnly bool, q *QueryOptions, connect, ingress bool) ([]*ServiceEntry, *QueryMeta, error) { func (h *Health) service(service string, tags []string, passingOnly bool, q *QueryOptions, healthType string) ([]*ServiceEntry, *QueryMeta, error) {
path := "/v1/health/service/" + service var path string
if connect || ingress { switch healthType {
case connectHealth:
path = "/v1/health/connect/" + service path = "/v1/health/connect/" + service
case ingressHealth:
path = "/v1/health/ingress/" + service
default:
path = "/v1/health/service/" + service
} }
r := h.c.newRequest("GET", path) r := h.c.newRequest("GET", path)
r.setQueryOptions(q) r.setQueryOptions(q)
if len(tags) > 0 { if len(tags) > 0 {
@ -315,9 +327,6 @@ func (h *Health) service(service string, tags []string, passingOnly bool, q *Que
if passingOnly { if passingOnly {
r.params.Set(HealthPassing, "1") r.params.Set(HealthPassing, "1")
} }
if ingress {
r.params.Set("ingress", "1")
}
rtt, resp, err := requireOK(h.c.doRequest(r)) rtt, resp, err := requireOK(h.c.doRequest(r))
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err

View File

@ -539,7 +539,7 @@ func TestAPI_HealthConnect_Filter(t *testing.T) {
require.Len(t, services, 1) require.Len(t, services, 1)
} }
func TestAPI_HealthConnect_Ingress(t *testing.T) { func TestAPI_HealthIngress(t *testing.T) {
t.Parallel() t.Parallel()
c, s := makeClient(t) c, s := makeClient(t)
defer s.Stop() defer s.Stop()