diff --git a/agent/agent_endpoint.go b/agent/agent_endpoint.go index 9092dbe483e3..6c97f1622785 100644 --- a/agent/agent_endpoint.go +++ b/agent/agent_endpoint.go @@ -20,6 +20,7 @@ import ( "github.com/hashicorp/consul/agent/checks" "github.com/hashicorp/consul/agent/config" "github.com/hashicorp/consul/agent/debug" + "github.com/hashicorp/consul/agent/local" "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/ipaddr" @@ -153,6 +154,62 @@ func (s *HTTPServer) AgentReload(resp http.ResponseWriter, req *http.Request) (i } } +func buildAgentService(s *structs.NodeService, proxies map[string]*local.ManagedProxy) api.AgentService { + weights := api.AgentWeights{Passing: 1, Warning: 1} + if s.Weights != nil { + if s.Weights.Passing > 0 { + weights.Passing = s.Weights.Passing + } + weights.Warning = s.Weights.Warning + } + as := api.AgentService{ + Kind: api.ServiceKind(s.Kind), + ID: s.ID, + Service: s.Service, + Tags: s.Tags, + Meta: s.Meta, + Port: s.Port, + Address: s.Address, + EnableTagOverride: s.EnableTagOverride, + CreateIndex: s.CreateIndex, + ModifyIndex: s.ModifyIndex, + Weights: weights, + } + + if as.Tags == nil { + as.Tags = []string{} + } + if as.Meta == nil { + as.Meta = map[string]string{} + } + // Attach Unmanaged Proxy config if exists + if s.Kind == structs.ServiceKindConnectProxy { + as.Proxy = s.Proxy.ToAPI() + // DEPRECATED (ProxyDestination) - remove this when removing ProxyDestination + // Also set the deprecated ProxyDestination + as.ProxyDestination = as.Proxy.DestinationServiceName + } + + // Attach Connect configs if they exist. We use the actual proxy state since + // that may have had defaults filled in compared to the config that was + // provided with the service as stored in the NodeService here. + if proxy, ok := proxies[s.ID+"-proxy"]; ok { + as.Connect = &api.AgentServiceConnect{ + Proxy: &api.AgentServiceConnectProxy{ + ExecMode: api.ProxyExecMode(proxy.Proxy.ExecMode.String()), + Command: proxy.Proxy.Command, + Config: proxy.Proxy.Config, + Upstreams: proxy.Proxy.Upstreams.ToAPI(), + }, + } + } else if s.Connect.Native { + as.Connect = &api.AgentServiceConnect{ + Native: true, + } + } + return as +} + func (s *HTTPServer) AgentServices(resp http.ResponseWriter, req *http.Request) (interface{}, error) { // Fetch the ACL token, if any. var token string @@ -173,59 +230,8 @@ func (s *HTTPServer) AgentServices(resp http.ResponseWriter, req *http.Request) // Use empty list instead of nil for id, s := range services { - weights := api.AgentWeights{Passing: 1, Warning: 1} - if s.Weights != nil { - if s.Weights.Passing > 0 { - weights.Passing = s.Weights.Passing - } - weights.Warning = s.Weights.Warning - } - as := &api.AgentService{ - Kind: api.ServiceKind(s.Kind), - ID: s.ID, - Service: s.Service, - Tags: s.Tags, - Meta: s.Meta, - Port: s.Port, - Address: s.Address, - EnableTagOverride: s.EnableTagOverride, - CreateIndex: s.CreateIndex, - ModifyIndex: s.ModifyIndex, - Weights: weights, - } - - if as.Tags == nil { - as.Tags = []string{} - } - if as.Meta == nil { - as.Meta = map[string]string{} - } - // Attach Unmanaged Proxy config if exists - if s.Kind == structs.ServiceKindConnectProxy { - as.Proxy = s.Proxy.ToAPI() - // DEPRECATED (ProxyDestination) - remove this when removing ProxyDestination - // Also set the deprecated ProxyDestination - as.ProxyDestination = as.Proxy.DestinationServiceName - } - - // Attach Connect configs if they exist. We use the actual proxy state since - // that may have had defaults filled in compared to the config that was - // provided with the service as stored in the NodeService here. - if proxy, ok := proxies[id+"-proxy"]; ok { - as.Connect = &api.AgentServiceConnect{ - Proxy: &api.AgentServiceConnectProxy{ - ExecMode: api.ProxyExecMode(proxy.Proxy.ExecMode.String()), - Command: proxy.Proxy.Command, - Config: proxy.Proxy.Config, - Upstreams: proxy.Proxy.Upstreams.ToAPI(), - }, - } - } else if s.Connect.Native { - as.Connect = &api.AgentServiceConnect{ - Native: true, - } - } - agentSvcs[id] = as + agentService := buildAgentService(s, proxies) + agentSvcs[id] = &agentService } return agentSvcs, nil @@ -704,6 +710,124 @@ func (s *HTTPServer) AgentCheckUpdate(resp http.ResponseWriter, req *http.Reques return nil, nil } +// agentHealthService Returns Health for a given service ID +func agentHealthService(serviceID string, s *HTTPServer) (int, string, api.HealthChecks) { + checks := s.agent.State.Checks() + serviceChecks := make(api.HealthChecks, 0) + for _, c := range checks { + if c.ServiceID == serviceID || c.ServiceID == "" { + // TODO: harmonize struct.HealthCheck and api.HealthCheck (or at least extract conversion function) + healthCheck := &api.HealthCheck{ + Node: c.Node, + CheckID: string(c.CheckID), + Name: c.Name, + Status: c.Status, + Notes: c.Notes, + Output: c.Output, + ServiceID: c.ServiceID, + ServiceName: c.ServiceName, + ServiceTags: c.ServiceTags, + } + serviceChecks = append(serviceChecks, healthCheck) + } + } + status := serviceChecks.AggregatedStatus() + switch status { + case api.HealthWarning: + return http.StatusTooManyRequests, status, serviceChecks + case api.HealthPassing: + return http.StatusOK, status, serviceChecks + default: + return http.StatusServiceUnavailable, status, serviceChecks + } +} + +func returnTextPlain(req *http.Request) bool { + if contentType := req.Header.Get("Accept"); strings.HasPrefix(contentType, "text/plain") { + return true + } + if format := req.URL.Query().Get("format"); format != "" { + return format == "text" + } + return false +} + +// AgentHealthServiceByID return the local Service Health given its ID +func (s *HTTPServer) AgentHealthServiceByID(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + // Pull out the service id (service id since there may be several instance of the same service on this host) + serviceID := strings.TrimPrefix(req.URL.Path, "/v1/agent/health/service/id/") + if serviceID == "" { + return nil, &BadRequestError{Reason: "Missing serviceID"} + } + services := s.agent.State.Services() + proxies := s.agent.State.Proxies() + for _, service := range services { + if service.ID == serviceID { + code, status, healthChecks := agentHealthService(serviceID, s) + if returnTextPlain(req) { + return status, api.CodeWithPayloadError{StatusCode: code, Reason: status, ContentType: "text/plain"} + } + serviceInfo := buildAgentService(service, proxies) + result := &api.AgentServiceChecksInfo{ + AggregatedStatus: status, + Checks: healthChecks, + Service: &serviceInfo, + } + return result, api.CodeWithPayloadError{StatusCode: code, Reason: status, ContentType: "application/json"} + } + } + notFoundReason := fmt.Sprintf("ServiceId %s not found", serviceID) + if returnTextPlain(req) { + return notFoundReason, api.CodeWithPayloadError{StatusCode: http.StatusNotFound, Reason: fmt.Sprintf("ServiceId %s not found", serviceID), ContentType: "application/json"} + } + return &api.AgentServiceChecksInfo{ + AggregatedStatus: api.HealthCritical, + Checks: nil, + Service: nil, + }, api.CodeWithPayloadError{StatusCode: http.StatusNotFound, Reason: notFoundReason, ContentType: "application/json"} +} + +// AgentHealthServiceByName return the worse status of all the services with given name on an agent +func (s *HTTPServer) AgentHealthServiceByName(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + // Pull out the service name + serviceName := strings.TrimPrefix(req.URL.Path, "/v1/agent/health/service/name/") + if serviceName == "" { + return nil, &BadRequestError{Reason: "Missing service Name"} + } + code := http.StatusNotFound + status := fmt.Sprintf("ServiceName %s Not Found", serviceName) + services := s.agent.State.Services() + result := make([]api.AgentServiceChecksInfo, 0, 16) + proxies := s.agent.State.Proxies() + for _, service := range services { + if service.Service == serviceName { + scode, sstatus, healthChecks := agentHealthService(service.ID, s) + serviceInfo := buildAgentService(service, proxies) + res := api.AgentServiceChecksInfo{ + AggregatedStatus: sstatus, + Checks: healthChecks, + Service: &serviceInfo, + } + result = append(result, res) + // When service is not found, we ignore it and keep existing HTTP status + if code == http.StatusNotFound { + code = scode + status = sstatus + } + // We take the worst of all statuses, so we keep iterating + // passing: 200 < warning: 429 < critical: 503 + if code < scode { + code = scode + status = sstatus + } + } + } + if returnTextPlain(req) { + return status, api.CodeWithPayloadError{StatusCode: code, Reason: status, ContentType: "text/plain"} + } + return result, api.CodeWithPayloadError{StatusCode: code, Reason: status, ContentType: "application/json"} +} + func (s *HTTPServer) AgentRegisterService(resp http.ResponseWriter, req *http.Request) (interface{}, error) { var args structs.ServiceDefinition // Fixup the type decode of TTL or Interval if a check if provided. diff --git a/agent/agent_endpoint_test.go b/agent/agent_endpoint_test.go index 07acd6e5d31b..6d36afdef9c8 100644 --- a/agent/agent_endpoint_test.go +++ b/agent/agent_endpoint_test.go @@ -627,6 +627,444 @@ func TestAgent_Checks(t *testing.T) { } } +func TestAgent_HealthServiceByID(t *testing.T) { + t.Parallel() + a := NewTestAgent(t.Name(), "") + defer a.Shutdown() + testrpc.WaitForTestAgent(t, a.RPC, "dc1") + + service := &structs.NodeService{ + ID: "mysql", + Service: "mysql", + } + if err := a.AddService(service, nil, false, "", ConfigSourceLocal); err != nil { + t.Fatalf("err: %v", err) + } + service = &structs.NodeService{ + ID: "mysql2", + Service: "mysql2", + } + if err := a.AddService(service, nil, false, "", ConfigSourceLocal); err != nil { + t.Fatalf("err: %v", err) + } + service = &structs.NodeService{ + ID: "mysql3", + Service: "mysql3", + } + if err := a.AddService(service, nil, false, "", ConfigSourceLocal); err != nil { + t.Fatalf("err: %v", err) + } + + chk1 := &structs.HealthCheck{ + Node: a.Config.NodeName, + CheckID: "mysql", + Name: "mysql", + ServiceID: "mysql", + Status: api.HealthPassing, + } + err := a.State.AddCheck(chk1, "") + if err != nil { + t.Fatalf("Err: %v", err) + } + + chk2 := &structs.HealthCheck{ + Node: a.Config.NodeName, + CheckID: "mysql", + Name: "mysql", + ServiceID: "mysql", + Status: api.HealthPassing, + } + err = a.State.AddCheck(chk2, "") + if err != nil { + t.Fatalf("Err: %v", err) + } + + chk3 := &structs.HealthCheck{ + Node: a.Config.NodeName, + CheckID: "mysql2", + Name: "mysql2", + ServiceID: "mysql2", + Status: api.HealthPassing, + } + err = a.State.AddCheck(chk3, "") + if err != nil { + t.Fatalf("Err: %v", err) + } + + chk4 := &structs.HealthCheck{ + Node: a.Config.NodeName, + CheckID: "mysql2", + Name: "mysql2", + ServiceID: "mysql2", + Status: api.HealthWarning, + } + err = a.State.AddCheck(chk4, "") + if err != nil { + t.Fatalf("Err: %v", err) + } + + chk5 := &structs.HealthCheck{ + Node: a.Config.NodeName, + CheckID: "mysql3", + Name: "mysql3", + ServiceID: "mysql3", + Status: api.HealthMaint, + } + err = a.State.AddCheck(chk5, "") + if err != nil { + t.Fatalf("Err: %v", err) + } + + chk6 := &structs.HealthCheck{ + Node: a.Config.NodeName, + CheckID: "mysql3", + Name: "mysql3", + ServiceID: "mysql3", + Status: api.HealthCritical, + } + err = a.State.AddCheck(chk6, "") + if err != nil { + t.Fatalf("Err: %v", err) + } + + eval := func(t *testing.T, url string, expectedCode int, expected string) { + t.Helper() + t.Run("format=text", func(t *testing.T) { + t.Helper() + req, _ := http.NewRequest("GET", url+"?format=text", nil) + resp := httptest.NewRecorder() + data, err := a.srv.AgentHealthServiceByID(resp, req) + codeWithPayload, ok := err.(api.CodeWithPayloadError) + if !ok { + t.Fatalf("Err: %v", err) + } + if got, want := codeWithPayload.StatusCode, expectedCode; got != want { + t.Fatalf("returned bad status: expected %d, but had: %d in %#v", expectedCode, codeWithPayload.StatusCode, codeWithPayload) + } + body, ok := data.(string) + if !ok { + t.Fatalf("Cannot get result as string in := %#v", data) + } + if got, want := body, expected; got != want { + t.Fatalf("got body %q want %q", got, want) + } + if got, want := codeWithPayload.Reason, expected; got != want { + t.Fatalf("got body %q want %q", got, want) + } + }) + t.Run("format=json", func(t *testing.T) { + req, _ := http.NewRequest("GET", url, nil) + resp := httptest.NewRecorder() + dataRaw, err := a.srv.AgentHealthServiceByID(resp, req) + codeWithPayload, ok := err.(api.CodeWithPayloadError) + if !ok { + t.Fatalf("Err: %v", err) + } + if got, want := codeWithPayload.StatusCode, expectedCode; got != want { + t.Fatalf("returned bad status: expected %d, but had: %d in %#v", expectedCode, codeWithPayload.StatusCode, codeWithPayload) + } + data, ok := dataRaw.(*api.AgentServiceChecksInfo) + if !ok { + t.Fatalf("Cannot connvert result to JSON: %#v", dataRaw) + } + if codeWithPayload.StatusCode != http.StatusNotFound { + if data != nil && data.AggregatedStatus != expected { + t.Fatalf("got body %v want %v", data, expected) + } + } + }) + } + + t.Run("passing checks", func(t *testing.T) { + eval(t, "/v1/agent/health/service/id/mysql", http.StatusOK, "passing") + }) + t.Run("warning checks", func(t *testing.T) { + eval(t, "/v1/agent/health/service/id/mysql2", http.StatusTooManyRequests, "warning") + }) + t.Run("critical checks", func(t *testing.T) { + eval(t, "/v1/agent/health/service/id/mysql3", http.StatusServiceUnavailable, "critical") + }) + t.Run("unknown serviceid", func(t *testing.T) { + eval(t, "/v1/agent/health/service/id/mysql1", http.StatusNotFound, "ServiceId mysql1 not found") + }) + + nodeCheck := &structs.HealthCheck{ + Node: a.Config.NodeName, + CheckID: "diskCheck", + Name: "diskCheck", + Status: api.HealthCritical, + } + err = a.State.AddCheck(nodeCheck, "") + + if err != nil { + t.Fatalf("Err: %v", err) + } + t.Run("critical check on node", func(t *testing.T) { + eval(t, "/v1/agent/health/service/id/mysql", http.StatusServiceUnavailable, "critical") + }) + + err = a.State.RemoveCheck(nodeCheck.CheckID) + if err != nil { + t.Fatalf("Err: %v", err) + } + nodeCheck = &structs.HealthCheck{ + Node: a.Config.NodeName, + CheckID: "_node_maintenance", + Name: "_node_maintenance", + Status: api.HealthMaint, + } + err = a.State.AddCheck(nodeCheck, "") + if err != nil { + t.Fatalf("Err: %v", err) + } + t.Run("maintenance check on node", func(t *testing.T) { + eval(t, "/v1/agent/health/service/id/mysql", http.StatusServiceUnavailable, "maintenance") + }) +} + +func TestAgent_HealthServiceByName(t *testing.T) { + t.Parallel() + a := NewTestAgent(t.Name(), "") + defer a.Shutdown() + + service := &structs.NodeService{ + ID: "mysql1", + Service: "mysql-pool-r", + } + if err := a.AddService(service, nil, false, "", ConfigSourceLocal); err != nil { + t.Fatalf("err: %v", err) + } + service = &structs.NodeService{ + ID: "mysql2", + Service: "mysql-pool-r", + } + if err := a.AddService(service, nil, false, "", ConfigSourceLocal); err != nil { + t.Fatalf("err: %v", err) + } + service = &structs.NodeService{ + ID: "mysql3", + Service: "mysql-pool-rw", + } + if err := a.AddService(service, nil, false, "", ConfigSourceLocal); err != nil { + t.Fatalf("err: %v", err) + } + service = &structs.NodeService{ + ID: "mysql4", + Service: "mysql-pool-rw", + } + if err := a.AddService(service, nil, false, "", ConfigSourceLocal); err != nil { + t.Fatalf("err: %v", err) + } + service = &structs.NodeService{ + ID: "httpd1", + Service: "httpd", + } + if err := a.AddService(service, nil, false, "", ConfigSourceLocal); err != nil { + t.Fatalf("err: %v", err) + } + service = &structs.NodeService{ + ID: "httpd2", + Service: "httpd", + } + if err := a.AddService(service, nil, false, "", ConfigSourceLocal); err != nil { + t.Fatalf("err: %v", err) + } + + chk1 := &structs.HealthCheck{ + Node: a.Config.NodeName, + CheckID: "mysql1", + Name: "mysql1", + ServiceID: "mysql1", + ServiceName: "mysql-pool-r", + Status: api.HealthPassing, + } + err := a.State.AddCheck(chk1, "") + if err != nil { + t.Fatalf("Err: %v", err) + } + + chk2 := &structs.HealthCheck{ + Node: a.Config.NodeName, + CheckID: "mysql1", + Name: "mysql1", + ServiceID: "mysql1", + ServiceName: "mysql-pool-r", + Status: api.HealthWarning, + } + err = a.State.AddCheck(chk2, "") + if err != nil { + t.Fatalf("Err: %v", err) + } + + chk3 := &structs.HealthCheck{ + Node: a.Config.NodeName, + CheckID: "mysql2", + Name: "mysql2", + ServiceID: "mysql2", + ServiceName: "mysql-pool-r", + Status: api.HealthPassing, + } + err = a.State.AddCheck(chk3, "") + if err != nil { + t.Fatalf("Err: %v", err) + } + + chk4 := &structs.HealthCheck{ + Node: a.Config.NodeName, + CheckID: "mysql2", + Name: "mysql2", + ServiceID: "mysql2", + ServiceName: "mysql-pool-r", + Status: api.HealthCritical, + } + err = a.State.AddCheck(chk4, "") + if err != nil { + t.Fatalf("Err: %v", err) + } + + chk5 := &structs.HealthCheck{ + Node: a.Config.NodeName, + CheckID: "mysql3", + Name: "mysql3", + ServiceID: "mysql3", + ServiceName: "mysql-pool-rw", + Status: api.HealthWarning, + } + err = a.State.AddCheck(chk5, "") + if err != nil { + t.Fatalf("Err: %v", err) + } + + chk6 := &structs.HealthCheck{ + Node: a.Config.NodeName, + CheckID: "mysql4", + Name: "mysql4", + ServiceID: "mysql4", + ServiceName: "mysql-pool-rw", + Status: api.HealthPassing, + } + err = a.State.AddCheck(chk6, "") + if err != nil { + t.Fatalf("Err: %v", err) + } + + chk7 := &structs.HealthCheck{ + Node: a.Config.NodeName, + CheckID: "httpd1", + Name: "httpd1", + ServiceID: "httpd1", + ServiceName: "httpd", + Status: api.HealthPassing, + } + err = a.State.AddCheck(chk7, "") + if err != nil { + t.Fatalf("Err: %v", err) + } + + chk8 := &structs.HealthCheck{ + Node: a.Config.NodeName, + CheckID: "httpd2", + Name: "httpd2", + ServiceID: "httpd2", + ServiceName: "httpd", + Status: api.HealthPassing, + } + err = a.State.AddCheck(chk8, "") + if err != nil { + t.Fatalf("Err: %v", err) + } + + eval := func(t *testing.T, url string, expectedCode int, expected string) { + t.Helper() + t.Run("format=text", func(t *testing.T) { + t.Helper() + req, _ := http.NewRequest("GET", url+"?format=text", nil) + resp := httptest.NewRecorder() + data, err := a.srv.AgentHealthServiceByName(resp, req) + codeWithPayload, ok := err.(api.CodeWithPayloadError) + if !ok { + t.Fatalf("Err: %v", err) + } + if got, want := codeWithPayload.StatusCode, expectedCode; got != want { + t.Fatalf("returned bad status: %d. Body: %q", resp.Code, resp.Body.String()) + } + if got, want := codeWithPayload.Reason, expected; got != want { + t.Fatalf("got reason %q want %q", got, want) + } + if got, want := data, expected; got != want { + t.Fatalf("got body %q want %q", got, want) + } + }) + t.Run("format=json", func(t *testing.T) { + t.Helper() + req, _ := http.NewRequest("GET", url, nil) + resp := httptest.NewRecorder() + dataRaw, err := a.srv.AgentHealthServiceByName(resp, req) + codeWithPayload, ok := err.(api.CodeWithPayloadError) + if !ok { + t.Fatalf("Err: %v", err) + } + data, ok := dataRaw.([]api.AgentServiceChecksInfo) + if !ok { + t.Fatalf("Cannot connvert result to JSON") + } + if got, want := codeWithPayload.StatusCode, expectedCode; got != want { + t.Fatalf("returned bad code: %d. Body: %#v", resp.Code, data) + } + if resp.Code != http.StatusNotFound { + if codeWithPayload.Reason != expected { + t.Fatalf("got wrong status %#v want %#v", codeWithPayload, expected) + } + } + }) + } + + t.Run("passing checks", func(t *testing.T) { + eval(t, "/v1/agent/health/service/name/httpd", http.StatusOK, "passing") + }) + t.Run("warning checks", func(t *testing.T) { + eval(t, "/v1/agent/health/service/name/mysql-pool-rw", http.StatusTooManyRequests, "warning") + }) + t.Run("critical checks", func(t *testing.T) { + eval(t, "/v1/agent/health/service/name/mysql-pool-r", http.StatusServiceUnavailable, "critical") + }) + t.Run("unknown serviceName", func(t *testing.T) { + eval(t, "/v1/agent/health/service/name/test", http.StatusNotFound, "ServiceName test Not Found") + }) + nodeCheck := &structs.HealthCheck{ + Node: a.Config.NodeName, + CheckID: "diskCheck", + Name: "diskCheck", + Status: api.HealthCritical, + } + err = a.State.AddCheck(nodeCheck, "") + + if err != nil { + t.Fatalf("Err: %v", err) + } + t.Run("critical check on node", func(t *testing.T) { + eval(t, "/v1/agent/health/service/name/mysql-pool-r", http.StatusServiceUnavailable, "critical") + }) + + err = a.State.RemoveCheck(nodeCheck.CheckID) + if err != nil { + t.Fatalf("Err: %v", err) + } + nodeCheck = &structs.HealthCheck{ + Node: a.Config.NodeName, + CheckID: "_node_maintenance", + Name: "_node_maintenance", + Status: api.HealthMaint, + } + err = a.State.AddCheck(nodeCheck, "") + if err != nil { + t.Fatalf("Err: %v", err) + } + t.Run("maintenance check on node", func(t *testing.T) { + eval(t, "/v1/agent/health/service/name/mysql-pool-r", http.StatusServiceUnavailable, "maintenance") + }) +} + func TestAgent_Checks_ACLFilter(t *testing.T) { t.Parallel() a := NewTestAgent(t.Name(), TestACLConfig()) diff --git a/agent/http.go b/agent/http.go index 73e9925a2ca1..11db5da5d771 100644 --- a/agent/http.go +++ b/agent/http.go @@ -19,6 +19,7 @@ import ( "github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/agent/cache" "github.com/hashicorp/consul/agent/structs" + "github.com/hashicorp/consul/api" "github.com/hashicorp/go-cleanhttp" "github.com/mitchellh/mapstructure" ) @@ -363,21 +364,41 @@ func (s *HTTPServer) wrap(handler endpoint, methods []string) http.HandlerFunc { // Invoke the handler obj, err = handler(resp, req) } - + contentType := "application/json" + httpCode := http.StatusOK if err != nil { - handleErr(err) - return + if errPayload, ok := err.(api.CodeWithPayloadError); ok { + httpCode = errPayload.StatusCode + if errPayload.ContentType != "" { + contentType = errPayload.ContentType + } + if errPayload.Reason != "" { + resp.Header().Add("X-Consul-Reason", errPayload.Reason) + } + } else { + handleErr(err) + return + } } if obj == nil { return } - - buf, err := s.marshalJSON(req, obj) - if err != nil { - handleErr(err) - return + var buf []byte + if contentType == "application/json" { + buf, err = s.marshalJSON(req, obj) + if err != nil { + handleErr(err) + return + } + } else { + if strings.HasPrefix(contentType, "text/") { + if val, ok := obj.(string); ok { + buf = []byte(val) + } + } } - resp.Header().Set("Content-Type", "application/json") + resp.Header().Set("Content-Type", contentType) + resp.WriteHeader(httpCode) resp.Write(buf) } } diff --git a/agent/http_oss.go b/agent/http_oss.go index 3d0a921b4b6f..e524e450a951 100644 --- a/agent/http_oss.go +++ b/agent/http_oss.go @@ -34,6 +34,8 @@ func init() { registerEndpoint("/v1/agent/join/", []string{"PUT"}, (*HTTPServer).AgentJoin) registerEndpoint("/v1/agent/leave", []string{"PUT"}, (*HTTPServer).AgentLeave) registerEndpoint("/v1/agent/force-leave/", []string{"PUT"}, (*HTTPServer).AgentForceLeave) + registerEndpoint("/v1/agent/health/service/id/", []string{"GET"}, (*HTTPServer).AgentHealthServiceByID) + registerEndpoint("/v1/agent/health/service/name/", []string{"GET"}, (*HTTPServer).AgentHealthServiceByName) registerEndpoint("/v1/agent/check/register", []string{"PUT"}, (*HTTPServer).AgentRegisterCheck) registerEndpoint("/v1/agent/check/deregister/", []string{"PUT"}, (*HTTPServer).AgentDeregisterCheck) registerEndpoint("/v1/agent/check/pass/", []string{"PUT"}, (*HTTPServer).AgentCheckPass) diff --git a/agent/structs/errors.go b/agent/structs/errors.go index 66337d2e4762..dcd2137c5696 100644 --- a/agent/structs/errors.go +++ b/agent/structs/errors.go @@ -12,6 +12,7 @@ const ( errNotReadyForConsistentReads = "Not ready to serve consistent reads" errSegmentsNotSupported = "Network segments are not supported in this version of Consul" errRPCRateExceeded = "RPC rate limit exceeded" + errServiceNotFound = "Service not found: " ) var ( @@ -30,3 +31,7 @@ func IsErrNoLeader(err error) bool { func IsErrRPCRateExceeded(err error) bool { return err != nil && strings.Contains(err.Error(), errRPCRateExceeded) } + +func IsErrServiceNotFound(err error) bool { + return err != nil && strings.Contains(err.Error(), errServiceNotFound) +}