diff --git a/agent/agent.go b/agent/agent.go index e15734b0fc..aca723e2ea 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -2911,6 +2911,7 @@ func (a *Agent) addCheck(check *structs.HealthCheck, chkType *structs.CheckType, HTTP: chkType.HTTP, Header: chkType.Header, Method: chkType.Method, + Body: chkType.Body, Interval: chkType.Interval, Timeout: chkType.Timeout, Logger: a.logger, diff --git a/agent/checks/check.go b/agent/checks/check.go index 9ae64be374..9c04ca4233 100644 --- a/agent/checks/check.go +++ b/agent/checks/check.go @@ -8,6 +8,7 @@ import ( "net/http" "os" osexec "os/exec" + "strings" "sync" "syscall" "time" @@ -333,6 +334,7 @@ type CheckHTTP struct { HTTP string Header map[string][]string Method string + Body string Interval time.Duration Timeout time.Duration Logger hclog.Logger @@ -355,6 +357,7 @@ func (c *CheckHTTP) CheckType() structs.CheckType { CheckID: c.CheckID.ID, HTTP: c.HTTP, Method: c.Method, + Body: c.Body, Header: c.Header, Interval: c.Interval, ProxyHTTP: c.ProxyHTTP, @@ -435,7 +438,8 @@ func (c *CheckHTTP) check() { target = c.ProxyHTTP } - req, err := http.NewRequest(method, target, nil) + bodyReader := strings.NewReader(c.Body) + req, err := http.NewRequest(method, target, bodyReader) if err != nil { c.StatusHandler.updateCheck(c.CheckID, api.HealthCritical, err.Error()) return diff --git a/agent/checks/check_test.go b/agent/checks/check_test.go index 95fa4d495c..406871bf61 100644 --- a/agent/checks/check_test.go +++ b/agent/checks/check_test.go @@ -586,6 +586,74 @@ func TestCheckHTTPTimeout(t *testing.T) { }) } +func TestCheckHTTPBody(t *testing.T) { + t.Parallel() + timeout := 5 * time.Millisecond + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var ( + buf bytes.Buffer + body []byte + ) + code := 200 + if _, err := buf.ReadFrom(r.Body); err != nil { + code = 999 + body = []byte(err.Error()) + } else { + body = buf.Bytes() + } + + w.WriteHeader(code) + w.Write(body) + })) + defer server.Close() + + tests := []struct { + desc string + method string + header http.Header + body string + }{ + {desc: "get body", method: "GET", body: "hello world"}, + {desc: "post body", method: "POST", body: "hello world"}, + {desc: "post json body", header: http.Header{"Content-Type": []string{"application/json"}}, method: "POST", body: "{\"foo\":\"bar\"}"}, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + notif := mock.NewNotify() + + cid := structs.NewCheckID("checkbody", nil) + logger := testutil.Logger(t) + check := &CheckHTTP{ + CheckID: cid, + HTTP: server.URL, + Header: tt.header, + Method: tt.method, + Body: tt.body, + Timeout: timeout, + Interval: 2 * time.Millisecond, + Logger: logger, + StatusHandler: NewStatusHandler(notif, logger, 0, 0), + } + check.Start() + defer check.Stop() + + retry.Run(t, func(r *retry.R) { + if got, want := notif.Updates(cid), 2; got < want { + r.Fatalf("got %d updates want at least %d", got, want) + } + if got, want := notif.State(cid), api.HealthPassing; got != want { + r.Fatalf("got status %q want %q", got, want) + } + if got, want := notif.Output(cid), tt.body; !strings.HasSuffix(got, want) { + r.Fatalf("got output %q want suffix %q", got, want) + } + }) + }) + } +} + func TestCheckHTTP_disablesKeepAlives(t *testing.T) { t.Parallel() notif := mock.NewNotify() diff --git a/agent/config/builder.go b/agent/config/builder.go index 8c90803e22..a802cfc78b 100644 --- a/agent/config/builder.go +++ b/agent/config/builder.go @@ -1287,6 +1287,7 @@ func (b *Builder) checkVal(v *CheckDefinition) *structs.CheckDefinition { HTTP: b.stringVal(v.HTTP), Header: v.Header, Method: b.stringVal(v.Method), + Body: b.stringVal(v.Body), TCP: b.stringVal(v.TCP), Interval: b.durationVal(fmt.Sprintf("check[%s].interval", id), v.Interval), DockerContainerID: b.stringVal(v.DockerContainerID), diff --git a/agent/config/config.go b/agent/config/config.go index a8e4361d4d..5bddf68702 100644 --- a/agent/config/config.go +++ b/agent/config/config.go @@ -418,6 +418,7 @@ type CheckDefinition struct { HTTP *string `json:"http,omitempty" hcl:"http" mapstructure:"http"` Header map[string][]string `json:"header,omitempty" hcl:"header" mapstructure:"header"` Method *string `json:"method,omitempty" hcl:"method" mapstructure:"method"` + Body *string `json:"body,omitempty" hcl:"body" mapstructure:"body"` OutputMaxSize *int `json:"output_max_size,omitempty" hcl:"output_max_size" mapstructure:"output_max_size"` TCP *string `json:"tcp,omitempty" hcl:"tcp" mapstructure:"tcp"` Interval *string `json:"interval,omitempty" hcl:"interval" mapstructure:"interval"` diff --git a/agent/config/runtime_test.go b/agent/config/runtime_test.go index bb1ce55b56..2407bfcf17 100644 --- a/agent/config/runtime_test.go +++ b/agent/config/runtime_test.go @@ -3738,6 +3738,7 @@ func TestFullConfig(t *testing.T) { "f3r6xFtM": [ "RyuIdDWv", "QbxEcIUM" ] }, "method": "Dou0nGT5", + "body": "5PBQd2OT", "output_max_size": ` + strconv.Itoa(checks.DefaultBufSize) + `, "tcp": "JY6fTTcw", "interval": "18714s", @@ -3763,6 +3764,7 @@ func TestFullConfig(t *testing.T) { "Ui0nU99X": [ "LMccm3Qe", "k5H5RggQ" ] }, "method": "aldrIQ4l", + "body": "wSjTy7dg", "tcp": "RJQND605", "interval": "22164s", "output_max_size": ` + strconv.Itoa(checks.DefaultBufSize) + `, @@ -3787,6 +3789,7 @@ func TestFullConfig(t *testing.T) { "qxvdnSE9": [ "6wBPUYdF", "YYh8wtSZ" ] }, "method": "gLrztrNw", + "body": "0jkKgGUC", "tcp": "4jG5casb", "interval": "28767s", "output_max_size": ` + strconv.Itoa(checks.DefaultBufSize) + `, @@ -4005,6 +4008,7 @@ func TestFullConfig(t *testing.T) { "l4HwQ112": ["fk56MNlo", "dhLK56aZ"] }, "method": "9afLm3Mj", + "body": "wVVL2V6f", "tcp": "fjiLFqVd", "interval": "23926s", "output_max_size": ` + strconv.Itoa(checks.DefaultBufSize) + `, @@ -4028,6 +4032,7 @@ func TestFullConfig(t *testing.T) { "SHOVq1Vv": [ "jntFhyym", "GYJh32pp" ] }, "method": "T66MFBfR", + "body": "OwGjTFQi", "tcp": "bNnNfx2A", "interval": "22224s", "output_max_size": ` + strconv.Itoa(checks.DefaultBufSize) + `, @@ -4050,6 +4055,7 @@ func TestFullConfig(t *testing.T) { "p2UI34Qz": [ "UsG1D0Qh", "NHhRiB6s" ] }, "method": "ciYHWors", + "body": "lUVLGYU7", "tcp": "FfvCwlqH", "interval": "12356s", "output_max_size": ` + strconv.Itoa(checks.DefaultBufSize) + `, @@ -4086,6 +4092,7 @@ func TestFullConfig(t *testing.T) { "cVFpko4u": ["gGqdEB6k", "9LsRo22u"] }, "method": "X5DrovFc", + "body": "WeikigLh", "tcp": "ICbxkpSF", "interval": "24392s", "output_max_size": ` + strconv.Itoa(checks.DefaultBufSize) + `, @@ -4125,6 +4132,7 @@ func TestFullConfig(t *testing.T) { "1UJXjVrT": [ "OJgxzTfk", "xZZrFsq7" ] }, "method": "5wkAxCUE", + "body": "7CRjCJyz", "tcp": "MN3oA9D2", "interval": "32718s", "output_max_size": ` + strconv.Itoa(checks.DefaultBufSize) + `, @@ -4147,6 +4155,7 @@ func TestFullConfig(t *testing.T) { "vr7wY7CS": [ "EtCoNPPL", "9vAarJ5s" ] }, "method": "wzByP903", + "body": "4I8ucZgZ", "tcp": "2exjZIGE", "interval": "5656s", "output_max_size": ` + strconv.Itoa(checks.DefaultBufSize) + `, @@ -4356,6 +4365,7 @@ func TestFullConfig(t *testing.T) { f3r6xFtM = [ "RyuIdDWv", "QbxEcIUM" ] } method = "Dou0nGT5" + body = "5PBQd2OT" tcp = "JY6fTTcw" interval = "18714s" output_max_size = ` + strconv.Itoa(checks.DefaultBufSize) + ` @@ -4381,6 +4391,7 @@ func TestFullConfig(t *testing.T) { "Ui0nU99X" = [ "LMccm3Qe", "k5H5RggQ" ] } method = "aldrIQ4l" + body = "wSjTy7dg" tcp = "RJQND605" interval = "22164s" output_max_size = ` + strconv.Itoa(checks.DefaultBufSize) + ` @@ -4405,6 +4416,7 @@ func TestFullConfig(t *testing.T) { "qxvdnSE9" = [ "6wBPUYdF", "YYh8wtSZ" ] } method = "gLrztrNw" + body = "0jkKgGUC" tcp = "4jG5casb" interval = "28767s" output_max_size = ` + strconv.Itoa(checks.DefaultBufSize) + ` @@ -4627,6 +4639,7 @@ func TestFullConfig(t *testing.T) { l4HwQ112 = [ "fk56MNlo", "dhLK56aZ" ] } method = "9afLm3Mj" + body = "wVVL2V6f" tcp = "fjiLFqVd" interval = "23926s" docker_container_id = "dO5TtRHk" @@ -4649,6 +4662,7 @@ func TestFullConfig(t *testing.T) { "SHOVq1Vv" = [ "jntFhyym", "GYJh32pp" ] } method = "T66MFBfR" + body = "OwGjTFQi" tcp = "bNnNfx2A" interval = "22224s" output_max_size = ` + strconv.Itoa(checks.DefaultBufSize) + ` @@ -4671,6 +4685,7 @@ func TestFullConfig(t *testing.T) { "p2UI34Qz" = [ "UsG1D0Qh", "NHhRiB6s" ] } method = "ciYHWors" + body = "lUVLGYU7" tcp = "FfvCwlqH" interval = "12356s" output_max_size = ` + strconv.Itoa(checks.DefaultBufSize) + ` @@ -4707,6 +4722,7 @@ func TestFullConfig(t *testing.T) { cVFpko4u = [ "gGqdEB6k", "9LsRo22u" ] } method = "X5DrovFc" + body = "WeikigLh" tcp = "ICbxkpSF" interval = "24392s" output_max_size = ` + strconv.Itoa(checks.DefaultBufSize) + ` @@ -4746,6 +4762,7 @@ func TestFullConfig(t *testing.T) { "1UJXjVrT" = [ "OJgxzTfk", "xZZrFsq7" ] } method = "5wkAxCUE" + body = "7CRjCJyz" tcp = "MN3oA9D2" interval = "32718s" output_max_size = ` + strconv.Itoa(checks.DefaultBufSize) + ` @@ -4768,6 +4785,7 @@ func TestFullConfig(t *testing.T) { "vr7wY7CS" = [ "EtCoNPPL", "9vAarJ5s" ] } method = "wzByP903" + body = "4I8ucZgZ" tcp = "2exjZIGE" interval = "5656s" output_max_size = ` + strconv.Itoa(checks.DefaultBufSize) + ` @@ -5071,6 +5089,7 @@ func TestFullConfig(t *testing.T) { "Ui0nU99X": []string{"LMccm3Qe", "k5H5RggQ"}, }, Method: "aldrIQ4l", + Body: "wSjTy7dg", TCP: "RJQND605", Interval: 22164 * time.Second, OutputMaxSize: checks.DefaultBufSize, @@ -5095,6 +5114,7 @@ func TestFullConfig(t *testing.T) { "qxvdnSE9": []string{"6wBPUYdF", "YYh8wtSZ"}, }, Method: "gLrztrNw", + Body: "0jkKgGUC", OutputMaxSize: checks.DefaultBufSize, TCP: "4jG5casb", Interval: 28767 * time.Second, @@ -5119,6 +5139,7 @@ func TestFullConfig(t *testing.T) { "f3r6xFtM": {"RyuIdDWv", "QbxEcIUM"}, }, Method: "Dou0nGT5", + Body: "5PBQd2OT", OutputMaxSize: checks.DefaultBufSize, TCP: "JY6fTTcw", Interval: 18714 * time.Second, @@ -5293,6 +5314,7 @@ func TestFullConfig(t *testing.T) { "cVFpko4u": {"gGqdEB6k", "9LsRo22u"}, }, Method: "X5DrovFc", + Body: "WeikigLh", OutputMaxSize: checks.DefaultBufSize, TCP: "ICbxkpSF", Interval: 24392 * time.Second, @@ -5342,6 +5364,7 @@ func TestFullConfig(t *testing.T) { "1UJXjVrT": {"OJgxzTfk", "xZZrFsq7"}, }, Method: "5wkAxCUE", + Body: "7CRjCJyz", OutputMaxSize: checks.DefaultBufSize, TCP: "MN3oA9D2", Interval: 32718 * time.Second, @@ -5364,6 +5387,7 @@ func TestFullConfig(t *testing.T) { "vr7wY7CS": {"EtCoNPPL", "9vAarJ5s"}, }, Method: "wzByP903", + Body: "4I8ucZgZ", OutputMaxSize: checks.DefaultBufSize, TCP: "2exjZIGE", Interval: 5656 * time.Second, @@ -5478,6 +5502,7 @@ func TestFullConfig(t *testing.T) { "SHOVq1Vv": {"jntFhyym", "GYJh32pp"}, }, Method: "T66MFBfR", + Body: "OwGjTFQi", OutputMaxSize: checks.DefaultBufSize, TCP: "bNnNfx2A", Interval: 22224 * time.Second, @@ -5500,6 +5525,7 @@ func TestFullConfig(t *testing.T) { "p2UI34Qz": {"UsG1D0Qh", "NHhRiB6s"}, }, Method: "ciYHWors", + Body: "lUVLGYU7", OutputMaxSize: checks.DefaultBufSize, TCP: "FfvCwlqH", Interval: 12356 * time.Second, @@ -5522,6 +5548,7 @@ func TestFullConfig(t *testing.T) { "l4HwQ112": {"fk56MNlo", "dhLK56aZ"}, }, Method: "9afLm3Mj", + Body: "wVVL2V6f", OutputMaxSize: checks.DefaultBufSize, TCP: "fjiLFqVd", Interval: 23926 * time.Second, @@ -5972,6 +5999,7 @@ func TestSanitize(t *testing.T) { "ID": "", "Interval": "0s", "Method": "", + "Body": "", "Name": "zoo", "Notes": "", "OutputMaxSize": ` + strconv.Itoa(checks.DefaultBufSize) + `, @@ -6155,6 +6183,7 @@ func TestSanitize(t *testing.T) { "Header": {}, "Interval": "0s", "Method": "", + "Body": "", "Name": "blurb", "Notes": "", "OutputMaxSize": ` + strconv.Itoa(checks.DefaultBufSize) + `, diff --git a/agent/http_decode_test.go b/agent/http_decode_test.go index 8f0553614d..2fc79334a0 100644 --- a/agent/http_decode_test.go +++ b/agent/http_decode_test.go @@ -2671,6 +2671,7 @@ func TestDecodeSessionCreate(t *testing.T) { // HTTP string // Header map[string][]string // Method string +// Body string // TLSSkipVerify bool // TCP string // IntervalDuration time.Duration diff --git a/agent/structs/check_definition.go b/agent/structs/check_definition.go index d00b1954f5..08119ab891 100644 --- a/agent/structs/check_definition.go +++ b/agent/structs/check_definition.go @@ -26,6 +26,7 @@ type CheckDefinition struct { HTTP string Header map[string][]string Method string + Body string TCP string Interval time.Duration DockerContainerID string @@ -171,6 +172,7 @@ func (c *CheckDefinition) CheckType() *CheckType { GRPCUseTLS: c.GRPCUseTLS, Header: c.Header, Method: c.Method, + Body: c.Body, OutputMaxSize: c.OutputMaxSize, TCP: c.TCP, Interval: c.Interval, diff --git a/agent/structs/check_type.go b/agent/structs/check_type.go index 7d87ed4471..e1b8f53f8c 100644 --- a/agent/structs/check_type.go +++ b/agent/structs/check_type.go @@ -34,6 +34,7 @@ type CheckType struct { HTTP string Header map[string][]string Method string + Body string TCP string Interval time.Duration AliasNode string diff --git a/agent/structs/structs.go b/agent/structs/structs.go index e7981d6009..b386cc4b6d 100644 --- a/agent/structs/structs.go +++ b/agent/structs/structs.go @@ -1318,6 +1318,7 @@ type HealthCheckDefinition struct { TLSSkipVerify bool `json:",omitempty"` Header map[string][]string `json:",omitempty"` Method string `json:",omitempty"` + Body string `json:",omitempty"` TCP string `json:",omitempty"` Interval time.Duration `json:",omitempty"` OutputMaxSize uint `json:",omitempty"` @@ -1463,6 +1464,7 @@ func (c *HealthCheck) CheckType() *CheckType { GRPCUseTLS: c.Definition.GRPCUseTLS, Header: c.Definition.Header, Method: c.Definition.Method, + Body: c.Definition.Body, TCP: c.Definition.TCP, Interval: c.Definition.Interval, DockerContainerID: c.Definition.DockerContainerID, diff --git a/agent/txn_endpoint.go b/agent/txn_endpoint.go index 1711d625ba..33ecadd150 100644 --- a/agent/txn_endpoint.go +++ b/agent/txn_endpoint.go @@ -240,6 +240,7 @@ func (s *HTTPServer) convertOps(resp http.ResponseWriter, req *http.Request) (st TLSSkipVerify: check.Definition.TLSSkipVerify, Header: check.Definition.Header, Method: check.Definition.Method, + Body: check.Definition.Body, TCP: check.Definition.TCP, Interval: interval, Timeout: timeout, diff --git a/api/agent.go b/api/agent.go index 9df6ad1df4..929d3ccd34 100644 --- a/api/agent.go +++ b/api/agent.go @@ -190,6 +190,7 @@ type AgentServiceCheck struct { HTTP string `json:",omitempty"` Header map[string][]string `json:",omitempty"` Method string `json:",omitempty"` + Body string `json:",omitempty"` TCP string `json:",omitempty"` Status string `json:",omitempty"` Notes string `json:",omitempty"` diff --git a/api/health.go b/api/health.go index 1d00701824..2f4894ae6e 100644 --- a/api/health.go +++ b/api/health.go @@ -51,6 +51,7 @@ type HealthCheckDefinition struct { HTTP string Header map[string][]string Method string + Body string TLSSkipVerify bool TCP string IntervalDuration time.Duration `json:"-"` diff --git a/website/source/api/agent/check.html.md b/website/source/api/agent/check.html.md index 553bcb76dd..6fec6ad6de 100644 --- a/website/source/api/agent/check.html.md +++ b/website/source/api/agent/check.html.md @@ -176,6 +176,8 @@ The table below shows this endpoint's support for - `Method` `(string: "")` - Specifies a different HTTP method to be used for an `HTTP` check. When no value is specified, `GET` is used. +- `Body` `(string: "")` - Specifies a body that should be sent with `HTTP` checks. + - `Header` `(map[string][]string: {})` - Specifies a set of headers that should be set for `HTTP` checks. Each header can have multiple values. @@ -221,7 +223,8 @@ The table below shows this endpoint's support for "Shell": "/bin/bash", "HTTP": "https://example.com", "Method": "POST", - "Header": {"x-foo":["bar", "baz"]}, + "Header": {"Content-Type": "application/json"}, + "Body": "{\"check\":\"mem\"}", "TCP": "example.com:22", "Interval": "10s", "Timeout": "5s", diff --git a/website/source/docs/agent/checks.html.md b/website/source/docs/agent/checks.html.md index e7a44c294c..544db3e3cf 100644 --- a/website/source/docs/agent/checks.html.md +++ b/website/source/docs/agent/checks.html.md @@ -156,7 +156,8 @@ A HTTP check: "http": "https://localhost:5000/health", "tls_skip_verify": false, "method": "POST", - "header": {"x-foo":["bar", "baz"]}, + "header": {"Content-Type": "application/json"}, + "body": "{\"method\":\"health\"}", "interval": "10s", "timeout": "1s" }