diff --git a/api/agent.go b/api/agent.go index 4b144fe18..e56a18dcd 100644 --- a/api/agent.go +++ b/api/agent.go @@ -68,6 +68,7 @@ type AgentServiceCheck struct { Timeout string `json:",omitempty"` TTL string `json:",omitempty"` HTTP string `json:",omitempty"` + Status string `json:",omitempty"` } type AgentServiceChecks []*AgentServiceCheck diff --git a/api/agent_test.go b/api/agent_test.go index dbd43f2d3..daeb7d6b0 100644 --- a/api/agent_test.go +++ b/api/agent_test.go @@ -59,6 +59,50 @@ func TestAgent_Services(t *testing.T) { t.Fatalf("err: %v", err) } + services, err := agent.Services() + if err != nil { + t.Fatalf("err: %v", err) + } + if _, ok := services["foo"]; !ok { + t.Fatalf("missing service: %v", services) + } + checks, err := agent.Checks() + if err != nil { + t.Fatalf("err: %v", err) + } + chk, ok := checks["service:foo"] + if !ok { + t.Fatalf("missing check: %v", checks) + } + + // Checks should default to critical + if chk.Status != "critical" { + t.Fatalf("Bad: %#v", chk) + } + + if err := agent.ServiceDeregister("foo"); err != nil { + t.Fatalf("err: %v", err) + } +} + +func TestAgent_Services_CheckPassing(t *testing.T) { + c, s := makeClient(t) + defer s.stop() + + agent := c.Agent() + reg := &AgentServiceRegistration{ + Name: "foo", + Tags: []string{"bar", "baz"}, + Port: 8000, + Check: &AgentServiceCheck{ + TTL: "15s", + Status: "passing", + }, + } + if err := agent.ServiceRegister(reg); err != nil { + t.Fatalf("err: %v", err) + } + services, err := agent.Services() if err != nil { t.Fatalf("err: %v", err) @@ -71,15 +115,38 @@ func TestAgent_Services(t *testing.T) { if err != nil { t.Fatalf("err: %v", err) } - if _, ok := checks["service:foo"]; !ok { + chk, ok := checks["service:foo"] + if !ok { t.Fatalf("missing check: %v", checks) } + if chk.Status != "passing" { + t.Fatalf("Bad: %#v", chk) + } if err := agent.ServiceDeregister("foo"); err != nil { t.Fatalf("err: %v", err) } } +func TestAgent_Services_CheckBadStatus(t *testing.T) { + c, s := makeClient(t) + defer s.stop() + + agent := c.Agent() + reg := &AgentServiceRegistration{ + Name: "foo", + Tags: []string{"bar", "baz"}, + Port: 8000, + Check: &AgentServiceCheck{ + TTL: "15s", + Status: "fluffy", + }, + } + if err := agent.ServiceRegister(reg); err == nil { + t.Fatalf("bad status accepted") + } +} + func TestAgent_ServiceAddress(t *testing.T) { t.Parallel() c, s := makeClient(t) @@ -231,9 +298,47 @@ func TestAgent_Checks(t *testing.T) { if err != nil { t.Fatalf("err: %v", err) } - if _, ok := checks["foo"]; !ok { + chk, ok := checks["foo"] + if !ok { t.Fatalf("missing check: %v", checks) } + if chk.Status != "critical" { + t.Fatalf("check not critical: %v", chk) + } + + if err := agent.CheckDeregister("foo"); err != nil { + t.Fatalf("err: %v", err) + } +} + +func TestAgent_CheckStartPassing(t *testing.T) { + c, s := makeClient(t) + defer s.stop() + + agent := c.Agent() + + reg := &AgentCheckRegistration{ + Name: "foo", + AgentServiceCheck: AgentServiceCheck{ + Status: "passing", + }, + } + reg.TTL = "15s" + if err := agent.CheckRegister(reg); err != nil { + t.Fatalf("err: %v", err) + } + + checks, err := agent.Checks() + if err != nil { + t.Fatalf("err: %v", err) + } + chk, ok := checks["foo"] + if !ok { + t.Fatalf("missing check: %v", checks) + } + if chk.Status != "passing" { + t.Fatalf("check not passing: %v", chk) + } if err := agent.CheckDeregister("foo"); err != nil { t.Fatalf("err: %v", err) diff --git a/command/agent/agent.go b/command/agent/agent.go index eb19875dd..ac83692ab 100644 --- a/command/agent/agent.go +++ b/command/agent/agent.go @@ -669,6 +669,9 @@ func (a *Agent) AddService(service *structs.NodeService, chkTypes CheckTypes, pe ServiceID: service.ID, ServiceName: service.Service, } + if chkType.Status != "" { + check.Status = chkType.Status + } if err := a.AddCheck(check, chkType, persist, token); err != nil { return err } diff --git a/command/agent/agent_endpoint.go b/command/agent/agent_endpoint.go index 18a855620..a4b5f6ca9 100644 --- a/command/agent/agent_endpoint.go +++ b/command/agent/agent_endpoint.go @@ -86,6 +86,12 @@ func (s *HTTPServer) AgentRegisterCheck(resp http.ResponseWriter, req *http.Requ return nil, nil } + if args.Status != "" && !structs.ValidStatus(args.Status) { + resp.WriteHeader(400) + resp.Write([]byte("Bad check status")) + return nil, nil + } + // Construct the health check health := args.HealthCheck(s.agent.config.NodeName) @@ -196,6 +202,11 @@ func (s *HTTPServer) AgentRegisterService(resp http.ResponseWriter, req *http.Re // Verify the check type chkTypes := args.CheckTypes() for _, check := range chkTypes { + if check.Status != "" && !structs.ValidStatus(check.Status) { + resp.WriteHeader(400) + resp.Write([]byte("Status for checks must 'passing', 'warning', 'critical', 'unknown'")) + return nil, nil + } if !check.Valid() { resp.WriteHeader(400) resp.Write([]byte("Must provide TTL or Script and Interval!")) diff --git a/command/agent/agent_endpoint_test.go b/command/agent/agent_endpoint_test.go index 78469f774..b0f7a481c 100644 --- a/command/agent/agent_endpoint_test.go +++ b/command/agent/agent_endpoint_test.go @@ -3,14 +3,15 @@ package agent import ( "errors" "fmt" - "github.com/hashicorp/consul/consul/structs" - "github.com/hashicorp/consul/testutil" - "github.com/hashicorp/serf/serf" "net/http" "net/http/httptest" "os" "testing" "time" + + "github.com/hashicorp/consul/consul/structs" + "github.com/hashicorp/consul/testutil" + "github.com/hashicorp/serf/serf" ) func TestHTTPAgentServices(t *testing.T) { @@ -285,6 +286,84 @@ func TestHTTPAgentRegisterCheck(t *testing.T) { if token := srv.agent.state.CheckToken("test"); token == "" { t.Fatalf("missing token") } + + // By default, checks start in critical state. + state := srv.agent.state.Checks()["test"] + if state.Status != structs.HealthCritical { + t.Fatalf("bad: %v", state) + } +} + +func TestHTTPAgentRegisterCheckPassing(t *testing.T) { + dir, srv := makeHTTPServer(t) + defer os.RemoveAll(dir) + defer srv.Shutdown() + defer srv.agent.Shutdown() + + // Register node + req, err := http.NewRequest("GET", "/v1/agent/check/register", nil) + if err != nil { + t.Fatalf("err: %v", err) + } + args := &CheckDefinition{ + Name: "test", + CheckType: CheckType{ + TTL: 15 * time.Second, + }, + Status: structs.HealthPassing, + } + req.Body = encodeReq(args) + + obj, err := srv.AgentRegisterCheck(nil, req) + if err != nil { + t.Fatalf("err: %v", err) + } + if obj != nil { + t.Fatalf("bad: %v", obj) + } + + // Ensure we have a check mapping + if _, ok := srv.agent.state.Checks()["test"]; !ok { + t.Fatalf("missing test check") + } + + if _, ok := srv.agent.checkTTLs["test"]; !ok { + t.Fatalf("missing test check ttl") + } + + state := srv.agent.state.Checks()["test"] + if state.Status != structs.HealthPassing { + t.Fatalf("bad: %v", state) + } +} + +func TestHTTPAgentRegisterCheckBadStatus(t *testing.T) { + dir, srv := makeHTTPServer(t) + defer os.RemoveAll(dir) + defer srv.Shutdown() + defer srv.agent.Shutdown() + + // Register node + req, err := http.NewRequest("GET", "/v1/agent/check/register", nil) + if err != nil { + t.Fatalf("err: %v", err) + } + args := &CheckDefinition{ + Name: "test", + CheckType: CheckType{ + TTL: 15 * time.Second, + }, + Status: "fluffy", + } + req.Body = encodeReq(args) + + resp := httptest.NewRecorder() + if _, err := srv.AgentRegisterCheck(resp, req); err != nil { + t.Fatalf("err: %v", err) + } + if resp.Code != 400 { + t.Fatalf("accepted bad status") + } } func TestHTTPAgentDeregisterCheck(t *testing.T) { diff --git a/command/agent/agent_test.go b/command/agent/agent_test.go index af1f206e2..92aad75ef 100644 --- a/command/agent/agent_test.go +++ b/command/agent/agent_test.go @@ -352,10 +352,53 @@ func TestAgent_AddCheck(t *testing.T) { } // Ensure we have a check mapping - if _, ok := agent.state.Checks()["mem"]; !ok { + sChk, ok := agent.state.Checks()["mem"] + if !ok { t.Fatalf("missing mem check") } + // Ensure our check is in the right state + if sChk.Status != structs.HealthCritical { + t.Fatalf("check not critical") + } + + // Ensure a TTL is setup + if _, ok := agent.checkMonitors["mem"]; !ok { + t.Fatalf("missing mem monitor") + } +} + +func TestAgent_AddCheck_StartPassing(t *testing.T) { + dir, agent := makeAgent(t, nextConfig()) + defer os.RemoveAll(dir) + defer agent.Shutdown() + + health := &structs.HealthCheck{ + Node: "foo", + CheckID: "mem", + Name: "memory util", + Status: structs.HealthPassing, + } + chk := &CheckType{ + Script: "exit 0", + Interval: 15 * time.Second, + } + err := agent.AddCheck(health, chk, false) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Ensure we have a check mapping + sChk, ok := agent.state.Checks()["mem"] + if !ok { + t.Fatalf("missing mem check") + } + + // Ensure our check is in the right state + if sChk.Status != structs.HealthPassing { + t.Fatalf("check not passing") + } + // Ensure a TTL is setup if _, ok := agent.checkMonitors["mem"]; !ok { t.Fatalf("missing mem monitor") diff --git a/command/agent/check.go b/command/agent/check.go index 67fae6233..82f83f6ba 100644 --- a/command/agent/check.go +++ b/command/agent/check.go @@ -39,6 +39,8 @@ type CheckType struct { Timeout time.Duration TTL time.Duration + Status string + Notes string } type CheckTypes []*CheckType diff --git a/command/agent/structs.go b/command/agent/structs.go index 06b969833..1d0e41e8b 100644 --- a/command/agent/structs.go +++ b/command/agent/structs.go @@ -47,6 +47,7 @@ type CheckDefinition struct { Notes string ServiceID string Token string + Status string CheckType `mapstructure:",squash"` } @@ -59,6 +60,9 @@ func (c *CheckDefinition) HealthCheck(node string) *structs.HealthCheck { Notes: c.Notes, ServiceID: c.ServiceID, } + if c.Status != "" { + health.Status = c.Status + } if health.CheckID == "" && health.Name != "" { health.CheckID = health.Name } diff --git a/consul/structs/structs.go b/consul/structs/structs.go index 58f39387d..60d980028 100644 --- a/consul/structs/structs.go +++ b/consul/structs/structs.go @@ -45,6 +45,12 @@ const ( HealthCritical = "critical" ) +func ValidStatus(s string) bool { + return s == HealthPassing || + s == HealthWarning || + s == HealthCritical +} + const ( // Client tokens have rules applied ACLTypeClient = "client"