Skip to content

Commit

Permalink
Merge pull request #1579 from hashicorp/f-choose-one
Browse files Browse the repository at this point in the history
nomad: add support for querying consistent subset of services
  • Loading branch information
shoenig authored May 24, 2022
2 parents 85deb5f + 22ee331 commit dd11964
Show file tree
Hide file tree
Showing 6 changed files with 219 additions and 25 deletions.
16 changes: 16 additions & 0 deletions dependency/dependency.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ type QueryOptions struct {
Datacenter string
Region string
Near string
Choose string
RequireConsistent bool
VaultGrace time.Duration
WaitIndex uint64
Expand Down Expand Up @@ -92,6 +93,10 @@ func (q *QueryOptions) Merge(o *QueryOptions) *QueryOptions {
r.Near = o.Near
}

if o.Choose != "" {
r.Choose = o.Choose
}

if o.RequireConsistent != false {
r.RequireConsistent = o.RequireConsistent
}
Expand Down Expand Up @@ -119,9 +124,16 @@ func (q *QueryOptions) ToConsulOpts() *consulapi.QueryOptions {
}

func (q *QueryOptions) ToNomadOpts() *nomadapi.QueryOptions {
var params map[string]string
if q.Choose != "" {
params = map[string]string{
"choose": q.Choose,
}
}
return &nomadapi.QueryOptions{
AllowStale: q.AllowStale,
Region: q.Region,
Params: params,
WaitIndex: q.WaitIndex,
WaitTime: q.WaitTime,
}
Expand All @@ -146,6 +158,10 @@ func (q *QueryOptions) String() string {
u.Add("near", q.Near)
}

if q.Choose != "" {
u.Add("choose", q.Choose)
}

if q.RequireConsistent {
u.Add("consistent", strconv.FormatBool(q.RequireConsistent))
}
Expand Down
23 changes: 23 additions & 0 deletions dependency/nomad_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ var (

// NomadServiceQueryRe is the regex that is used to understand a service
// specific Nomad query.
//
// e.g. "<tag=value>.<name>@<region>"
NomadServiceQueryRe = regexp.MustCompile(`\A` + tagRe + serviceNameRe + regionRe + `\z`)
)

Expand Down Expand Up @@ -46,6 +48,7 @@ type NomadServiceQuery struct {
region string
name string
tag string
choose string
}

// NewNomadServiceQuery parses a string into a NomadServiceQuery which is
Expand All @@ -56,6 +59,7 @@ func NewNomadServiceQuery(s string) (*NomadServiceQuery, error) {
}

m := regexpMatch(NomadServiceQueryRe, s)

return &NomadServiceQuery{
stopCh: make(chan struct{}, 1),
region: m["region"],
Expand All @@ -64,6 +68,21 @@ func NewNomadServiceQuery(s string) (*NomadServiceQuery, error) {
}, nil
}

// NewNomadServiceChooseQuery parses s using NewNomadServiceQuery, and then also
// configures the resulting query with the choose parameter set according to the
// count and key arguments.
func NewNomadServiceChooseQuery(count int, key, s string) (*NomadServiceQuery, error) {
query, err := NewNomadServiceQuery(s)
if err != nil {
return nil, err
}

choose := fmt.Sprintf("%d|%s", count, key)
query.choose = choose

return query, nil
}

// Fetch queries the Nomad API defined by the given client and returns a slice
// of NomadService objects.
func (d *NomadServiceQuery) Fetch(client *ClientSet, opts *QueryOptions) (interface{}, *ResponseMetadata, error) {
Expand All @@ -75,6 +94,7 @@ func (d *NomadServiceQuery) Fetch(client *ClientSet, opts *QueryOptions) (interf

opts = opts.Merge(&QueryOptions{
Region: d.region,
Choose: d.choose,
})

u := &url.URL{
Expand Down Expand Up @@ -138,6 +158,9 @@ func (d *NomadServiceQuery) String() string {
if d.region != "" {
name = name + "@" + d.region
}
if d.choose != "" {
name = name + ":" + d.choose
}
return fmt.Sprintf("nomad.service(%s)", name)
}

Expand Down
96 changes: 93 additions & 3 deletions dependency/nomad_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
)

func TestNewNomadServiceQuery(t *testing.T) {

cases := []struct {
name string
i string
Expand Down Expand Up @@ -97,8 +96,20 @@ func TestNewNomadServiceQuery(t *testing.T) {
}
}

func TestNomadServiceQuery_Fetch(t *testing.T) {
func TestNomadServiceChooseQuery(t *testing.T) {
query, err := NewNomadServiceChooseQuery(4, "abc123", "tag.name@us-east-1")
require.NoError(t, err)

query.stopCh = nil
require.Equal(t, &NomadServiceQuery{
region: "us-east-1",
name: "name",
tag: "tag",
choose: "4|abc123",
}, query)
}

func TestNomadServiceQuery_Fetch(t *testing.T) {
cases := []struct {
name string
i string
Expand Down Expand Up @@ -185,8 +196,59 @@ func TestNomadServiceQuery_Fetch(t *testing.T) {
}
}

func TestNomadServiceQuery_String(t *testing.T) {
func TestNomadServicesQuery_Fetch_3arg(t *testing.T) {
cases := []struct {
name string
service string
count int
key string
exp []*NomadService
}{
{
name: "choose one",
service: "example-cache",
count: 1,
key: "abc123",
exp: []*NomadService{
&NomadService{
Name: "example-cache",
Address: "127.0.0.1",
Datacenter: "dc1",
Tags: ServiceTags([]string{"tag1", "tag2"}),
JobID: "example",
},
},
},
}

for i, tc := range cases {
t.Run(fmt.Sprintf("%d_%s", i, tc.name), func(t *testing.T) {
d, err := NewNomadServiceChooseQuery(tc.count, tc.key, tc.service)
if err != nil {
t.Fatal(err)
}

actI, _, err := d.Fetch(testClients, nil)
if err != nil {
t.Fatal(err)
}

act := actI.([]*NomadService)

for _, s := range act {
// clear random fields
s.ID = ""
s.Node = ""
s.Port = 0
s.AllocID = ""
}

require.Equal(t, tc.exp, act)
})
}
}

func TestNomadServiceQuery_String(t *testing.T) {
cases := []struct {
name string
i string
Expand Down Expand Up @@ -224,3 +286,31 @@ func TestNomadServiceQuery_String(t *testing.T) {
})
}
}

func TestNomadServiceQuery_String_3arg(t *testing.T) {
cases := []struct {
name string
i string
count int
key string
exp string
}{
{
"choose",
"redis",
3,
"abc123",
"nomad.service(redis:3|abc123)",
},
}

for i, tc := range cases {
t.Run(fmt.Sprintf("%d_%s", i, tc.name), func(t *testing.T) {
d, err := NewNomadServiceChooseQuery(3, "abc123", tc.i)
if err != nil {
t.Fatal(err)
}
require.Equal(t, tc.exp, d.String())
})
}
}
18 changes: 9 additions & 9 deletions dependency/nomad_services_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,17 +53,16 @@ func TestNewNomadServicesQueryQuery(t *testing.T) {
}
}

func TestNomadServicesQuery_Fetch(t *testing.T) {

func TestNomadServicesQuery_Fetch_1arg(t *testing.T) {
cases := []struct {
name string
i string
exp []*NomadServicesSnippet
name string
service string
exp []*NomadServicesSnippet
}{
{
"all",
"",
[]*NomadServicesSnippet{
name: "all",
service: "",
exp: []*NomadServicesSnippet{
&NomadServicesSnippet{
Name: "example-cache",
Tags: ServiceTags([]string{"tag1", "tag2"}),
Expand All @@ -74,7 +73,7 @@ func TestNomadServicesQuery_Fetch(t *testing.T) {

for i, tc := range cases {
t.Run(fmt.Sprintf("%d_%s", i, tc.name), func(t *testing.T) {
d, err := NewNomadServicesQuery(tc.i)
d, err := NewNomadServicesQuery(tc.service)
if err != nil {
t.Fatal(err)
}
Expand All @@ -88,6 +87,7 @@ func TestNomadServicesQuery_Fetch(t *testing.T) {
})
}
}

func TestNomadServicesQuery_String(t *testing.T) {

cases := []struct {
Expand Down
41 changes: 41 additions & 0 deletions docs/templating-language.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ provides the following functions:
- [modulo](#modulo)
- [minimum](#minimum)
- [maximum](#maximum)
- [Nomad Functions](#nomad-functions)
- [nomadServices](#nomadservices)
- [nomadService](#nomadservice)
- [Debugging Functions](#debugging)
- [spew_dump](#spew_dump)
- [spew_sdump](#spew_sdump)
Expand Down Expand Up @@ -1780,6 +1783,44 @@ This can also be used with a pipe function.
{{ 5 | maximum 2 }} // 2
```

## Nomad Functions

Nomad service registrations can be queried using the `nomadServices` and `nomadService` functions.
Typically these will be used from within a Nomad [template](https://www.nomadproject.io/docs/job-specification/template#nomad-services) configuration.

### `nomadServices`

This can be used to query the names of services registered in Nomad.

```nomadServices
{{ range nomadServices }}
{{ .Name .Tags }}
{{ end }}
```

### `nomadService`

This can be used to query for additional information about each instance of a service registered in Nomad.

```nomadService
{{ range nomadService "my-app" }}
{{ .Address }} {{ .Port }}
{{ end}}
```

The `nomadService` function also supports basic load-balancing via a [rendezvous hashing](https://en.wikipedia.org/wiki/Rendezvous_hashing)
algorithm implemented in Nomad's API. To activate this behavior, the function requires three arguments in this order:
the number of instances desired, a unique but consistent identifier associated with the requester, and the service name.

Typically the unique identifier would be the allocation ID in a Nomad job.

```nomadService
{{$allocID := env "NOMAD_ALLOC_ID" -}}
{{range nomadService 3 $allocID "redis"}}
{{.Address}} {{.Port}} | {{.Tags}} @ {{.Datacenter}}
{{- end}}
```

## Debugging Functions

Debugging functions help template developers understand the current context of a template block. These
Expand Down
Loading

0 comments on commit dd11964

Please sign in to comment.