From 25e38c82570263e922163969cd1ce28d13fe2bd3 Mon Sep 17 00:00:00 2001 From: Jerome Gravel-Niquet Date: Fri, 23 Aug 2019 12:49:02 -0400 Subject: [PATCH] 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 --- api/services.go | 1 + client/allocrunner/taskrunner/service_hook.go | 9 ++++++ command/agent/consul/client.go | 13 ++++++--- command/agent/job_endpoint.go | 2 ++ command/agent/job_endpoint_test.go | 12 ++++++++ jobspec/parse_service.go | 28 +++++++++++++++---- jobspec/parse_test.go | 27 ++++++++++++++++++ jobspec/test-fixtures/service-meta.hcl | 14 ++++++++++ nomad/structs/services.go | 18 +++++++++--- website/source/api/jobs.html.md | 3 ++ website/source/api/json-jobs.html.md | 6 ++++ .../docs/job-specification/service.html.md | 7 +++++ 12 files changed, 126 insertions(+), 14 deletions(-) create mode 100644 jobspec/test-fixtures/service-meta.hcl diff --git a/api/services.go b/api/services.go index 1db24098b..91ce8626b 100644 --- a/api/services.go +++ b/api/services.go @@ -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 diff --git a/client/allocrunner/taskrunner/service_hook.go b/client/allocrunner/taskrunner/service_hook.go index a89110417..edea8673c 100644 --- a/client/allocrunner/taskrunner/service_hook.go +++ b/client/allocrunner/taskrunner/service_hook.go @@ -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 } diff --git a/command/agent/consul/client.go b/command/agent/consul/client.go index ca09961f6..ac05d6bf3 100644 --- a/command/agent/consul/client.go +++ b/command/agent/consul/client.go @@ -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) diff --git a/command/agent/job_endpoint.go b/command/agent/job_endpoint.go index 18c438d33..d2c70a37d 100644 --- a/command/agent/job_endpoint.go +++ b/command/agent/job_endpoint.go @@ -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 { diff --git a/command/agent/job_endpoint_test.go b/command/agent/job_endpoint_test.go index 8ace3ba05..e1934e29e 100644 --- a/command/agent/job_endpoint_test.go +++ b/command/agent/job_endpoint_test.go @@ -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", diff --git a/jobspec/parse_service.go b/jobspec/parse_service.go index 94d88d6e3..1ab1d0438 100644 --- a/jobspec/parse_service.go +++ b/jobspec/parse_service.go @@ -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 } diff --git a/jobspec/parse_test.go b/jobspec/parse_test.go index 79a6f7d43..cd3857388 100644 --- a/jobspec/parse_test.go +++ b/jobspec/parse_test.go @@ -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{ diff --git a/jobspec/test-fixtures/service-meta.hcl b/jobspec/test-fixtures/service-meta.hcl new file mode 100644 index 000000000..abf87e2a4 --- /dev/null +++ b/jobspec/test-fixtures/service-meta.hcl @@ -0,0 +1,14 @@ +job "service_meta" { + type = "service" + group "group" { + task "task" { + service { + name = "http-service" + meta { + foo = "bar" + } + } + } + } +} + diff --git a/nomad/structs/services.go b/nomad/structs/services.go index 3eccb53bf..267c22450 100644 --- a/nomad/structs/services.go +++ b/nomad/structs/services.go @@ -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 } diff --git a/website/source/api/jobs.html.md b/website/source/api/jobs.html.md index 412726356..d94271010 100644 --- a/website/source/api/jobs.html.md +++ b/website/source/api/jobs.html.md @@ -146,6 +146,9 @@ The table below shows this endpoint's support for "global", "cache" ], + "Meta": { + "meta": "for my service" + }, "PortLabel": "db", "AddressMode": "", "Checks": [{ diff --git a/website/source/api/json-jobs.html.md b/website/source/api/json-jobs.html.md index d8cd4b601..1ed91c1f9 100644 --- a/website/source/api/json-jobs.html.md +++ b/website/source/api/json-jobs.html.md @@ -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 diff --git a/website/source/docs/job-specification/service.html.md b/website/source/docs/job-specification/service.html.md index 878f4ea05..ab7a68b13 100644 --- a/website/source/docs/job-specification/service.html.md +++ b/website/source/docs/job-specification/service.html.md @@ -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` ([Meta][]: nil) - Specifies a key-value map that annotates + the Consul service with user-defined metadata. ### `check` Parameters