Skip to content

Commit 0a4ea47

Browse files
authored
Merge pull request #3031 from hashicorp/f-2924-consul-headers
Add Header and Method support for HTTP checks
2 parents 879eb31 + 948d869 commit 0a4ea47

33 files changed

+1108
-284
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ IMPROVEMENTS:
1313
* deployment: Emit task events explaining unhealthy allocations[GH-3025]
1414
* deployment: Better description when a deployment should auto-revert but there
1515
is no target [GH-3024]
16+
* discovery: Add HTTP header and method support to checks [GH-3031]
1617
* driver/docker: Added DNS options [GH-2992]
1718
* driver/rkt: support read-only volume mounts [GH-2883]
1819
* jobspec: Add `shutdown_delay` so tasks can delay shutdown after

api/tasks.go

+2
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ type ServiceCheck struct {
9494
Timeout time.Duration
9595
InitialStatus string `mapstructure:"initial_status"`
9696
TLSSkipVerify bool `mapstructure:"tls_skip_verify"`
97+
Header map[string][]string
98+
Method string
9799
}
98100

99101
// The Service model represents a Consul service definition

client/task_runner.go

+12
Original file line numberDiff line numberDiff line change
@@ -1450,6 +1450,18 @@ func interpolateServices(taskEnv *env.TaskEnv, task *structs.Task) *structs.Task
14501450
check.Protocol = taskEnv.ReplaceEnv(check.Protocol)
14511451
check.PortLabel = taskEnv.ReplaceEnv(check.PortLabel)
14521452
check.InitialStatus = taskEnv.ReplaceEnv(check.InitialStatus)
1453+
check.Method = taskEnv.ReplaceEnv(check.Method)
1454+
if len(check.Header) > 0 {
1455+
header := make(map[string][]string, len(check.Header))
1456+
for k, vs := range check.Header {
1457+
newVals := make([]string, len(vs))
1458+
for i, v := range vs {
1459+
newVals[i] = taskEnv.ReplaceEnv(v)
1460+
}
1461+
header[taskEnv.ReplaceEnv(k)] = newVals
1462+
}
1463+
check.Header = header
1464+
}
14531465
}
14541466
service.Name = taskEnv.ReplaceEnv(service.Name)
14551467
service.PortLabel = taskEnv.ReplaceEnv(service.PortLabel)

client/task_runner_test.go

+84
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"os"
1010
"path/filepath"
1111
"reflect"
12+
"strings"
1213
"syscall"
1314
"testing"
1415
"time"
@@ -17,11 +18,13 @@ import (
1718
"github.com/golang/snappy"
1819
"github.com/hashicorp/nomad/client/allocdir"
1920
"github.com/hashicorp/nomad/client/config"
21+
"github.com/hashicorp/nomad/client/driver/env"
2022
cstructs "github.com/hashicorp/nomad/client/structs"
2123
"github.com/hashicorp/nomad/client/vaultclient"
2224
"github.com/hashicorp/nomad/nomad/mock"
2325
"github.com/hashicorp/nomad/nomad/structs"
2426
"github.com/hashicorp/nomad/testutil"
27+
"github.com/kr/pretty"
2528
)
2629

2730
func testLogger() *log.Logger {
@@ -1615,6 +1618,87 @@ func TestTaskRunner_Pre06ScriptCheck(t *testing.T) {
16151618
t.Run(run("0.5.6", "mock_driver", "tcp", false))
16161619
}
16171620

1621+
func TestTaskRunner_interpolateServices(t *testing.T) {
1622+
t.Parallel()
1623+
task := &structs.Task{
1624+
Services: []*structs.Service{
1625+
{
1626+
Name: "${name}",
1627+
PortLabel: "${portlabel}",
1628+
Tags: []string{"${tags}"},
1629+
Checks: []*structs.ServiceCheck{
1630+
{
1631+
Name: "${checkname}",
1632+
Type: "${checktype}",
1633+
Command: "${checkcmd}",
1634+
Args: []string{"${checkarg}"},
1635+
Path: "${checkstr}",
1636+
Protocol: "${checkproto}",
1637+
PortLabel: "${checklabel}",
1638+
InitialStatus: "${checkstatus}",
1639+
Method: "${checkmethod}",
1640+
Header: map[string][]string{
1641+
"${checkheaderk}": {"${checkheaderv}"},
1642+
},
1643+
},
1644+
},
1645+
},
1646+
},
1647+
}
1648+
1649+
env := &env.TaskEnv{
1650+
EnvMap: map[string]string{
1651+
"name": "name",
1652+
"portlabel": "portlabel",
1653+
"tags": "tags",
1654+
"checkname": "checkname",
1655+
"checktype": "checktype",
1656+
"checkcmd": "checkcmd",
1657+
"checkarg": "checkarg",
1658+
"checkstr": "checkstr",
1659+
"checkpath": "checkpath",
1660+
"checkproto": "checkproto",
1661+
"checklabel": "checklabel",
1662+
"checkstatus": "checkstatus",
1663+
"checkmethod": "checkmethod",
1664+
"checkheaderk": "checkheaderk",
1665+
"checkheaderv": "checkheaderv",
1666+
},
1667+
}
1668+
1669+
interpTask := interpolateServices(env, task)
1670+
1671+
exp := &structs.Task{
1672+
Services: []*structs.Service{
1673+
{
1674+
Name: "name",
1675+
PortLabel: "portlabel",
1676+
Tags: []string{"tags"},
1677+
Checks: []*structs.ServiceCheck{
1678+
{
1679+
Name: "checkname",
1680+
Type: "checktype",
1681+
Command: "checkcmd",
1682+
Args: []string{"checkarg"},
1683+
Path: "checkstr",
1684+
Protocol: "checkproto",
1685+
PortLabel: "checklabel",
1686+
InitialStatus: "checkstatus",
1687+
Method: "checkmethod",
1688+
Header: map[string][]string{
1689+
"checkheaderk": {"checkheaderv"},
1690+
},
1691+
},
1692+
},
1693+
},
1694+
},
1695+
}
1696+
1697+
if diff := pretty.Diff(interpTask, exp); len(diff) > 0 {
1698+
t.Fatalf("diff:\n%s\n", strings.Join(diff, "\n"))
1699+
}
1700+
}
1701+
16181702
func TestTaskRunner_ShutdownDelay(t *testing.T) {
16191703
t.Parallel()
16201704

command/agent/consul/client.go

+2
Original file line numberDiff line numberDiff line change
@@ -1002,6 +1002,8 @@ func createCheckReg(serviceID, checkID string, check *structs.ServiceCheck, host
10021002
}
10031003
url := base.ResolveReference(relative)
10041004
chkReg.HTTP = url.String()
1005+
chkReg.Method = check.Method
1006+
chkReg.Header = check.Header
10051007
case structs.ServiceCheckTCP:
10061008
chkReg.TCP = net.JoinHostPort(host, strconv.Itoa(port))
10071009
case structs.ServiceCheckScript:

command/agent/consul/unit_test.go

+46
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@ import (
77
"log"
88
"os"
99
"reflect"
10+
"strings"
1011
"sync"
1112
"testing"
1213
"time"
1314

1415
"github.com/hashicorp/consul/api"
1516
cstructs "github.com/hashicorp/nomad/client/structs"
1617
"github.com/hashicorp/nomad/nomad/structs"
18+
"github.com/kr/pretty"
1719
)
1820

1921
const (
@@ -1352,3 +1354,47 @@ func TestIsNomadService(t *testing.T) {
13521354
})
13531355
}
13541356
}
1357+
1358+
// TestCreateCheckReg asserts Nomad ServiceCheck structs are properly converted
1359+
// to Consul API AgentCheckRegistrations.
1360+
func TestCreateCheckReg(t *testing.T) {
1361+
check := &structs.ServiceCheck{
1362+
Name: "name",
1363+
Type: "http",
1364+
Path: "/path",
1365+
PortLabel: "label",
1366+
Method: "POST",
1367+
Header: map[string][]string{
1368+
"Foo": {"bar"},
1369+
},
1370+
}
1371+
1372+
serviceID := "testService"
1373+
checkID := check.Hash(serviceID)
1374+
host := "localhost"
1375+
port := 41111
1376+
1377+
expected := &api.AgentCheckRegistration{
1378+
ID: checkID,
1379+
Name: "name",
1380+
ServiceID: serviceID,
1381+
AgentServiceCheck: api.AgentServiceCheck{
1382+
Timeout: "0s",
1383+
Interval: "0s",
1384+
HTTP: fmt.Sprintf("http://%s:%d/path", host, port),
1385+
Method: "POST",
1386+
Header: map[string][]string{
1387+
"Foo": {"bar"},
1388+
},
1389+
},
1390+
}
1391+
1392+
actual, err := createCheckReg(serviceID, checkID, check, host, port)
1393+
if err != nil {
1394+
t.Fatalf("err: %v", err)
1395+
}
1396+
1397+
if diff := pretty.Diff(actual, expected); len(diff) > 0 {
1398+
t.Fatalf("diff:\n%s\n", strings.Join(diff, "\n"))
1399+
}
1400+
}

command/agent/job_endpoint.go

+2
Original file line numberDiff line numberDiff line change
@@ -699,6 +699,8 @@ func ApiTaskToStructsTask(apiTask *api.Task, structsTask *structs.Task) {
699699
Timeout: check.Timeout,
700700
InitialStatus: check.InitialStatus,
701701
TLSSkipVerify: check.TLSSkipVerify,
702+
Header: check.Header,
703+
Method: check.Method,
702704
}
703705
}
704706
}

helper/funcs.go

+15
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,21 @@ func CopyMapStringFloat64(m map[string]float64) map[string]float64 {
212212
return c
213213
}
214214

215+
// CopyMapStringSliceString copies a map of strings to string slices such as
216+
// http.Header
217+
func CopyMapStringSliceString(m map[string][]string) map[string][]string {
218+
l := len(m)
219+
if l == 0 {
220+
return nil
221+
}
222+
223+
c := make(map[string][]string, l)
224+
for k, v := range m {
225+
c[k] = CopySliceString(v)
226+
}
227+
return c
228+
}
229+
215230
func CopySliceString(s []string) []string {
216231
l := len(s)
217232
if l == 0 {

helper/funcs_test.go

+18
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,24 @@ func TestMapStringStringSliceValueSet(t *testing.T) {
3636
}
3737
}
3838

39+
func TestCopyMapStringSliceString(t *testing.T) {
40+
m := map[string][]string{
41+
"x": []string{"a", "b", "c"},
42+
"y": []string{"1", "2", "3"},
43+
"z": nil,
44+
}
45+
46+
c := CopyMapStringSliceString(m)
47+
if !reflect.DeepEqual(c, m) {
48+
t.Fatalf("%#v != %#v", m, c)
49+
}
50+
51+
c["x"][1] = "---"
52+
if reflect.DeepEqual(c, m) {
53+
t.Fatalf("Shared slices: %#v == %#v", m["x"], c["x"])
54+
}
55+
}
56+
3957
func TestClearEnvVar(t *testing.T) {
4058
type testCase struct {
4159
input string

jobspec/parse.go

+33
Original file line numberDiff line numberDiff line change
@@ -962,6 +962,8 @@ func parseChecks(service *api.Service, checkObjs *ast.ObjectList) error {
962962
"args",
963963
"initial_status",
964964
"tls_skip_verify",
965+
"header",
966+
"method",
965967
}
966968
if err := checkHCLKeys(co.Val, valid); err != nil {
967969
return multierror.Prefix(err, "check ->")
@@ -972,6 +974,37 @@ func parseChecks(service *api.Service, checkObjs *ast.ObjectList) error {
972974
if err := hcl.DecodeObject(&cm, co.Val); err != nil {
973975
return err
974976
}
977+
978+
// HCL allows repeating stanzas so merge 'header' into a single
979+
// map[string][]string.
980+
if headerI, ok := cm["header"]; ok {
981+
headerRaw, ok := headerI.([]map[string]interface{})
982+
if !ok {
983+
return fmt.Errorf("check -> header -> expected a []map[string][]string but found %T", headerI)
984+
}
985+
m := map[string][]string{}
986+
for _, rawm := range headerRaw {
987+
for k, vI := range rawm {
988+
vs, ok := vI.([]interface{})
989+
if !ok {
990+
return fmt.Errorf("check -> header -> %q expected a []string but found %T", k, vI)
991+
}
992+
for _, vI := range vs {
993+
v, ok := vI.(string)
994+
if !ok {
995+
return fmt.Errorf("check -> header -> %q expected a string but found %T", k, vI)
996+
}
997+
m[k] = append(m[k], v)
998+
}
999+
}
1000+
}
1001+
1002+
check.Header = m
1003+
1004+
// Remove "header" as it has been parsed
1005+
delete(cm, "header")
1006+
}
1007+
9751008
dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
9761009
DecodeHook: mapstructure.StringToTimeDurationHookFunc(),
9771010
WeaklyTypedInput: true,

jobspec/parse_test.go

+15
Original file line numberDiff line numberDiff line change
@@ -450,9 +450,14 @@ func TestParse(t *testing.T) {
450450
{
451451
Name: "check-name",
452452
Type: "http",
453+
Path: "/",
453454
Interval: 10 * time.Second,
454455
Timeout: 2 * time.Second,
455456
InitialStatus: capi.HealthPassing,
457+
Method: "POST",
458+
Header: map[string][]string{
459+
"Authorization": {"Basic ZWxhc3RpYzpjaGFuZ2VtZQ=="},
460+
},
456461
},
457462
},
458463
},
@@ -464,6 +469,16 @@ func TestParse(t *testing.T) {
464469
},
465470
false,
466471
},
472+
{
473+
"service-check-bad-header.hcl",
474+
nil,
475+
true,
476+
},
477+
{
478+
"service-check-bad-header-2.hcl",
479+
nil,
480+
true,
481+
},
467482
{
468483
// TODO This should be pushed into the API
469484
"vault_inheritance.hcl",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
job "check_bad_header" {
2+
type = "service"
3+
group "group" {
4+
count = 1
5+
6+
task "task" {
7+
service {
8+
tags = ["bar"]
9+
port = "http"
10+
11+
check {
12+
name = "check-name"
13+
type = "http"
14+
path = "/"
15+
method = "POST"
16+
interval = "10s"
17+
timeout = "2s"
18+
initial_status = "passing"
19+
20+
header {
21+
Authorization = ["ok", 840]
22+
}
23+
}
24+
}
25+
}
26+
}
27+
}
28+

0 commit comments

Comments
 (0)