Consul service meta (#6193)

* adds meta object to service in job spec, sends it to consul

* adds tests for service meta

* fix tests

* adds docs

* better hashing for service meta, use helper for copying meta when registering service

* tried to be DRY, but looks like it would be more work to use the
helper function
This commit is contained in:
Jerome Gravel-Niquet
2019-08-23 12:49:02 -04:00
committed by Nick Ethier
parent 974ff0392c
commit 25e38c8257
12 changed files with 126 additions and 14 deletions

View File

@@ -106,6 +106,7 @@ type Service struct {
Checks []ServiceCheck
CheckRestart *CheckRestart `mapstructure:"check_restart"`
Connect *ConsulConnect
Meta map[string]string
}
// Canonicalize the Service by ensuring its name and address mode are set. Task

View File

@@ -252,6 +252,15 @@ func interpolateServices(taskEnv *taskenv.TaskEnv, services []*structs.Service)
service.PortLabel = taskEnv.ReplaceEnv(service.PortLabel)
service.Tags = taskEnv.ParseAndReplace(service.Tags)
service.CanaryTags = taskEnv.ParseAndReplace(service.CanaryTags)
if len(service.Meta) > 0 {
meta := make(map[string]string, len(service.Meta))
for k, v := range service.Meta {
meta[k] = taskEnv.ReplaceEnv(v)
}
service.Meta = meta
}
interpolated[i] = service
}

View File

@@ -728,6 +728,14 @@ func (c *ServiceClient) serviceRegs(ops *operations, service *structs.Service, t
return nil, fmt.Errorf("invalid Consul Connect configuration for service %q: %v", service.Name, err)
}
meta := make(map[string]string, len(service.Meta))
for k, v := range service.Meta {
meta[k] = v
}
// This enables the consul UI to show that Nomad registered this service
meta["external-source"] = "nomad"
// Build the Consul Service registration request
serviceReg := &api.AgentServiceRegistration{
ID: id,
@@ -735,10 +743,7 @@ func (c *ServiceClient) serviceRegs(ops *operations, service *structs.Service, t
Tags: tags,
Address: ip,
Port: port,
// This enables the consul UI to show that Nomad registered this service
Meta: map[string]string{
"external-source": "nomad",
},
Meta: meta,
Connect: connect, // will be nil if no Connect stanza
}
ops.regServices = append(ops.regServices, serviceReg)

View File

@@ -828,6 +828,7 @@ func ApiTaskToStructsTask(apiTask *api.Task, structsTask *structs.Task) {
Tags: service.Tags,
CanaryTags: service.CanaryTags,
AddressMode: service.AddressMode,
Meta: helper.CopyMapStringString(service.Meta),
}
if l := len(service.Checks); l != 0 {
@@ -1005,6 +1006,7 @@ func ApiServicesToStructs(in []*api.Service) []*structs.Service {
Tags: s.Tags,
CanaryTags: s.CanaryTags,
AddressMode: s.AddressMode,
Meta: helper.CopyMapStringString(s.Meta),
}
if l := len(s.Checks); l != 0 {

View File

@@ -1507,6 +1507,9 @@ func TestJobs_ApiJobToStructsJob(t *testing.T) {
Tags: []string{"a", "b"},
CanaryTags: []string{"d", "e"},
PortLabel: "1234",
Meta: map[string]string{
"servicemeta": "foobar",
},
CheckRestart: &api.CheckRestart{
Limit: 4,
Grace: helper.TimeToPtr(11 * time.Second),
@@ -1571,6 +1574,9 @@ func TestJobs_ApiJobToStructsJob(t *testing.T) {
Tags: []string{"1", "2"},
CanaryTags: []string{"3", "4"},
PortLabel: "foo",
Meta: map[string]string{
"servicemeta": "foobar",
},
CheckRestart: &api.CheckRestart{
Limit: 4,
Grace: helper.TimeToPtr(11 * time.Second),
@@ -1845,6 +1851,9 @@ func TestJobs_ApiJobToStructsJob(t *testing.T) {
CanaryTags: []string{"d", "e"},
PortLabel: "1234",
AddressMode: "auto",
Meta: map[string]string{
"servicemeta": "foobar",
},
Checks: []*structs.ServiceCheck{
{
Name: "bar",
@@ -1904,6 +1913,9 @@ func TestJobs_ApiJobToStructsJob(t *testing.T) {
CanaryTags: []string{"3", "4"},
PortLabel: "foo",
AddressMode: "auto",
Meta: map[string]string{
"servicemeta": "foobar",
},
Checks: []*structs.ServiceCheck{
{
Name: "bar",

View File

@@ -46,6 +46,7 @@ func parseService(o *ast.ObjectItem) (*api.Service, error) {
"address_mode",
"check_restart",
"connect",
"meta",
}
if err := helper.CheckHCLKeys(o.Val, valid); err != nil {
return nil, err
@@ -60,27 +61,28 @@ func parseService(o *ast.ObjectItem) (*api.Service, error) {
delete(m, "check")
delete(m, "check_restart")
delete(m, "connect")
delete(m, "meta")
if err := mapstructure.WeakDecode(m, &service); err != nil {
return nil, err
}
// Filter checks
var checkList *ast.ObjectList
// Filter list
var listVal *ast.ObjectList
if ot, ok := o.Val.(*ast.ObjectType); ok {
checkList = ot.List
listVal = ot.List
} else {
return nil, fmt.Errorf("'%s': should be an object", service.Name)
}
if co := checkList.Filter("check"); len(co.Items) > 0 {
if co := listVal.Filter("check"); len(co.Items) > 0 {
if err := parseChecks(&service, co); err != nil {
return nil, multierror.Prefix(err, fmt.Sprintf("'%s',", service.Name))
}
}
// Filter check_restart
if cro := checkList.Filter("check_restart"); len(cro.Items) > 0 {
if cro := listVal.Filter("check_restart"); len(cro.Items) > 0 {
if len(cro.Items) > 1 {
return nil, fmt.Errorf("check_restart '%s': cannot have more than 1 check_restart", service.Name)
}
@@ -93,7 +95,7 @@ func parseService(o *ast.ObjectItem) (*api.Service, error) {
}
// Filter connect
if co := checkList.Filter("connect"); len(co.Items) > 0 {
if co := listVal.Filter("connect"); len(co.Items) > 0 {
if len(co.Items) > 1 {
return nil, fmt.Errorf("connect '%s': cannot have more than 1 connect stanza", service.Name)
}
@@ -106,6 +108,20 @@ func parseService(o *ast.ObjectItem) (*api.Service, error) {
service.Connect = c
}
// Parse out meta fields. These are in HCL as a list so we need
// to iterate over them and merge them.
if metaO := listVal.Filter("meta"); len(metaO.Items) > 0 {
for _, o := range metaO.Elem().Items {
var m map[string]interface{}
if err := hcl.DecodeObject(&m, o.Val); err != nil {
return nil, err
}
if err := mapstructure.WeakDecode(m, &service.Meta); err != nil {
return nil, err
}
}
}
return &service, nil
}

View File

@@ -784,6 +784,33 @@ func TestParse(t *testing.T) {
},
false,
},
{
"service-meta.hcl",
&api.Job{
ID: helper.StringToPtr("service_meta"),
Name: helper.StringToPtr("service_meta"),
Type: helper.StringToPtr("service"),
TaskGroups: []*api.TaskGroup{
{
Name: helper.StringToPtr("group"),
Tasks: []*api.Task{
{
Name: "task",
Services: []*api.Service{
{
Name: "http-service",
Meta: map[string]string{
"foo": "bar",
},
},
},
},
},
},
},
},
false,
},
{
"reschedule-job.hcl",
&api.Job{

View File

@@ -0,0 +1,14 @@
job "service_meta" {
type = "service"
group "group" {
task "task" {
service {
name = "http-service"
meta {
foo = "bar"
}
}
}
}
}

View File

@@ -324,10 +324,11 @@ type Service struct {
// this service.
AddressMode string
Tags []string // List of tags for the service
CanaryTags []string // List of tags for the service when it is a canary
Checks []*ServiceCheck // List of checks associated with the service
Connect *ConsulConnect // Consul Connect configuration
Tags []string // List of tags for the service
CanaryTags []string // List of tags for the service when it is a canary
Checks []*ServiceCheck // List of checks associated with the service
Connect *ConsulConnect // Consul Connect configuration
Meta map[string]string // Consul service meta
}
// Copy the stanza recursively. Returns nil if nil.
@@ -350,6 +351,8 @@ func (s *Service) Copy() *Service {
ns.Connect = s.Connect.Copy()
ns.Meta = helper.CopyMapStringString(s.Meta)
return ns
}
@@ -458,6 +461,9 @@ func (s *Service) Hash(allocID, taskName string, canary bool) string {
for _, tag := range s.CanaryTags {
io.WriteString(h, tag)
}
if len(s.Meta) > 0 {
fmt.Fprintf(h, "%v", s.Meta)
}
// Vary ID on whether or not CanaryTags will be used
if canary {
@@ -514,6 +520,10 @@ OUTER:
return false
}
if !reflect.DeepEqual(s.Meta, o.Meta) {
return false
}
if !helper.CompareSliceSetString(s.Tags, o.Tags) {
return false
}

View File

@@ -146,6 +146,9 @@ The table below shows this endpoint's support for
"global",
"cache"
],
"Meta": {
"meta": "for my service"
},
"PortLabel": "db",
"AddressMode": "",
"Checks": [{

View File

@@ -56,6 +56,9 @@ Below is the JSON representation of the job outputted by `$ nomad init`:
"global",
"cache"
],
"Meta": {
"meta": "for my service",
},
"PortLabel": "db",
"AddressMode": "",
"Checks": [{
@@ -400,6 +403,9 @@ The `Task` object supports the following keys:
- `Tags`: A list of string tags associated with this Service. String
interpolation is supported in tags.
- `Meta`: A key-value map that annotates the Consul service with
user-defined metadata. String interpolation is supported in meta.
- `CanaryTags`: A list of string tags associated with this Service while it
is a canary. Once the canary is promoted, the registered tags will be

View File

@@ -33,6 +33,10 @@ job "docs" {
port = "db"
meta {
meta = "for your service"
}
check {
type = "tcp"
port = "db"
@@ -135,6 +139,9 @@ does not automatically enable service discovery.
implemented for Docker and rkt.
- `host` - Use the host IP and port.
- `meta` <code>([Meta][]: nil)</code> - Specifies a key-value map that annotates
the Consul service with user-defined metadata.
### `check` Parameters