diff --git a/CHANGELOG.md b/CHANGELOG.md index 38d94ca3d..0765c39d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,7 @@ IMPROVEMENTS: * api: Added JSON representation of rules to policy endpoint response [[GH-6017](https://github.com/hashicorp/nomad/pull/6017)] * api: Update policy endpoint to permit anonymous access [[GH-6021](https://github.com/hashicorp/nomad/issues/6021)] * build: Updated to Go 1.12.13 [[GH-6606](https://github.com/hashicorp/nomad/issues/6606)] + * consul: Add support for service `canary_meta` * cli: Show full ID in node and alloc individual status views [[GH-6425](https://github.com/hashicorp/nomad/issues/6425)] * client: Enable setting tags on Consul Connect sidecar service [[GH-6448](https://github.com/hashicorp/nomad/issues/6448)] * client: Added support for downloading artifacts from Google Cloud Storage [[GH-6692](https://github.com/hashicorp/nomad/pull/6692)] diff --git a/api/services.go b/api/services.go index 8630126ce..6b1220ea3 100644 --- a/api/services.go +++ b/api/services.go @@ -107,6 +107,7 @@ type Service struct { CheckRestart *CheckRestart `mapstructure:"check_restart"` Connect *ConsulConnect Meta map[string]string + CanaryMeta map[string]string } // Canonicalize the Service by ensuring its name and address mode are set. Task diff --git a/command/agent/consul/catalog_testing.go b/command/agent/consul/catalog_testing.go index 621d41439..235f419ce 100644 --- a/command/agent/consul/catalog_testing.go +++ b/command/agent/consul/catalog_testing.go @@ -5,6 +5,7 @@ import ( "sync" log "github.com/hashicorp/go-hclog" + "github.com/hashicorp/nomad/helper" "github.com/hashicorp/consul/api" ) @@ -111,6 +112,7 @@ func (c *MockAgent) Services() (map[string]*api.AgentService, error) { ID: v.ID, Service: v.Name, Tags: make([]string, len(v.Tags)), + Meta: helper.CopyMapStringString(v.Meta), Port: v.Port, Address: v.Address, EnableTagOverride: v.EnableTagOverride, diff --git a/command/agent/consul/client.go b/command/agent/consul/client.go index e7480a8b7..88965c6f9 100644 --- a/command/agent/consul/client.go +++ b/command/agent/consul/client.go @@ -99,7 +99,8 @@ func agentServiceUpdateRequired(reg *api.AgentServiceRegistration, svc *api.Agen reg.Port == svc.Port && reg.Address == svc.Address && reg.Name == svc.Service && - reflect.DeepEqual(reg.Tags, svc.Tags)) + reflect.DeepEqual(reg.Tags, svc.Tags) && + reflect.DeepEqual(reg.Meta, svc.Meta)) } // operations are submitted to the main loop via commit() for synchronizing @@ -713,9 +714,17 @@ func (c *ServiceClient) serviceRegs(ops *operations, service *structs.Service, w 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 + var meta map[string]string + if task.Canary && len(service.CanaryMeta) > 0 { + meta = make(map[string]string, len(service.CanaryMeta)+1) + for k, v := range service.CanaryMeta { + meta[k] = v + } + } else { + meta = make(map[string]string, len(service.Meta)+1) + for k, v := range service.Meta { + meta[k] = v + } } // This enables the consul UI to show that Nomad registered this service diff --git a/command/agent/consul/unit_test.go b/command/agent/consul/unit_test.go index 3cc702f00..65ab5aa66 100644 --- a/command/agent/consul/unit_test.go +++ b/command/agent/consul/unit_test.go @@ -35,6 +35,7 @@ func testWorkload() *WorkloadServices { Name: "taskname-service", PortLabel: "x", Tags: []string{"tag1", "tag2"}, + Meta: map[string]string{"meta1": "foo"}, }, }, Networks: []*structs.NetworkResource{ @@ -1077,6 +1078,73 @@ func TestConsul_CanaryTags_NoTags(t *testing.T) { require.Len(ctx.FakeConsul.services, 0) } +// TestConsul_CanaryMeta asserts CanaryMeta are used when Canary=true +func TestConsul_CanaryMeta(t *testing.T) { + t.Parallel() + require := require.New(t) + ctx := setupFake(t) + + canaryMeta := map[string]string{"meta1": "canary"} + canaryMeta["external-source"] = "nomad" + ctx.Task.Canary = true + ctx.Task.Services[0].CanaryMeta = canaryMeta + + require.NoError(ctx.ServiceClient.RegisterTask(ctx.Task)) + require.NoError(ctx.syncOnce()) + require.Len(ctx.FakeConsul.services, 1) + for _, service := range ctx.FakeConsul.services { + require.Equal(canaryMeta, service.Meta) + } + + // Disable canary and assert meta are not the canary meta + origTask := ctx.Task.Copy() + ctx.Task.Canary = false + require.NoError(ctx.ServiceClient.UpdateTask(origTask, ctx.Task)) + require.NoError(ctx.syncOnce()) + require.Len(ctx.FakeConsul.services, 1) + for _, service := range ctx.FakeConsul.services { + require.NotEqual(canaryMeta, service.Meta) + } + + ctx.ServiceClient.RemoveTask(ctx.Task) + require.NoError(ctx.syncOnce()) + require.Len(ctx.FakeConsul.services, 0) +} + +// TestConsul_CanaryMeta_NoMeta asserts Meta are used when Canary=true and there +// are no specified canary meta +func TestConsul_CanaryMeta_NoMeta(t *testing.T) { + t.Parallel() + require := require.New(t) + ctx := setupFake(t) + + meta := map[string]string{"meta1": "foo"} + meta["external-source"] = "nomad" + ctx.Task.Canary = true + ctx.Task.Services[0].Meta = meta + + require.NoError(ctx.ServiceClient.RegisterTask(ctx.Task)) + require.NoError(ctx.syncOnce()) + require.Len(ctx.FakeConsul.services, 1) + for _, service := range ctx.FakeConsul.services { + require.Equal(meta, service.Meta) + } + + // Disable canary and assert meta dont change + origTask := ctx.Task.Copy() + ctx.Task.Canary = false + require.NoError(ctx.ServiceClient.UpdateTask(origTask, ctx.Task)) + require.NoError(ctx.syncOnce()) + require.Len(ctx.FakeConsul.services, 1) + for _, service := range ctx.FakeConsul.services { + require.Equal(meta, service.Meta) + } + + ctx.ServiceClient.RemoveTask(ctx.Task) + require.NoError(ctx.syncOnce()) + require.Len(ctx.FakeConsul.services, 0) +} + // TestConsul_PeriodicSync asserts that Nomad periodically reconciles with // Consul. func TestConsul_PeriodicSync(t *testing.T) { diff --git a/command/agent/job_endpoint.go b/command/agent/job_endpoint.go index 808e3ff14..1ce796eb2 100644 --- a/command/agent/job_endpoint.go +++ b/command/agent/job_endpoint.go @@ -834,6 +834,7 @@ func ApiTaskToStructsTask(apiTask *api.Task, structsTask *structs.Task) { CanaryTags: service.CanaryTags, AddressMode: service.AddressMode, Meta: helper.CopyMapStringString(service.Meta), + CanaryMeta: helper.CopyMapStringString(service.CanaryMeta), } if l := len(service.Checks); l != 0 { @@ -1012,6 +1013,7 @@ func ApiServicesToStructs(in []*api.Service) []*structs.Service { CanaryTags: s.CanaryTags, AddressMode: s.AddressMode, Meta: helper.CopyMapStringString(s.Meta), + CanaryMeta: helper.CopyMapStringString(s.CanaryMeta), } if l := len(s.Checks); l != 0 { diff --git a/jobspec/parse_service.go b/jobspec/parse_service.go index 783b006bc..a974527f0 100644 --- a/jobspec/parse_service.go +++ b/jobspec/parse_service.go @@ -47,6 +47,7 @@ func parseService(o *ast.ObjectItem) (*api.Service, error) { "check_restart", "connect", "meta", + "canary_meta", } if err := helper.CheckHCLKeys(o.Val, valid); err != nil { return nil, err @@ -62,6 +63,7 @@ func parseService(o *ast.ObjectItem) (*api.Service, error) { delete(m, "check_restart") delete(m, "connect") delete(m, "meta") + delete(m, "canary_meta") if err := mapstructure.WeakDecode(m, &service); err != nil { return nil, err @@ -122,6 +124,20 @@ func parseService(o *ast.ObjectItem) (*api.Service, error) { } } + // Parse out canary_meta fields. These are in HCL as a list so we need + // to iterate over them and merge them. + if metaO := listVal.Filter("canary_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.CanaryMeta); err != nil { + return nil, err + } + } + } + return &service, nil } diff --git a/jobspec/parse_test.go b/jobspec/parse_test.go index 9bac757df..d4cea8803 100644 --- a/jobspec/parse_test.go +++ b/jobspec/parse_test.go @@ -218,7 +218,13 @@ func TestParse(t *testing.T) { { Tags: []string{"foo", "bar"}, CanaryTags: []string{"canary", "bam"}, - PortLabel: "http", + Meta: map[string]string{ + "abc": "123", + }, + CanaryMeta: map[string]string{ + "canary": "boom", + }, + PortLabel: "http", Checks: []api.ServiceCheck{ { Name: "check-name", diff --git a/jobspec/test-fixtures/basic.hcl b/jobspec/test-fixtures/basic.hcl index 19b8a5964..57aeba418 100644 --- a/jobspec/test-fixtures/basic.hcl +++ b/jobspec/test-fixtures/basic.hcl @@ -169,6 +169,14 @@ job "binstore-storagelocker" { } service { + meta { + abc = "123" + } + + canary_meta { + canary = "boom" + } + tags = ["foo", "bar"] canary_tags = ["canary", "bam"] port = "http" diff --git a/nomad/structs/services.go b/nomad/structs/services.go index abc79256e..e3b40c279 100644 --- a/nomad/structs/services.go +++ b/nomad/structs/services.go @@ -331,6 +331,7 @@ type Service struct { Checks []*ServiceCheck // List of checks associated with the service Connect *ConsulConnect // Consul Connect configuration Meta map[string]string // Consul service meta + CanaryMeta map[string]string // Consul service meta when it is a canary } // Copy the stanza recursively. Returns nil if nil. @@ -354,6 +355,7 @@ func (s *Service) Copy() *Service { ns.Connect = s.Connect.Copy() ns.Meta = helper.CopyMapStringString(s.Meta) + ns.CanaryMeta = helper.CopyMapStringString(s.CanaryMeta) return ns } @@ -466,6 +468,9 @@ func (s *Service) Hash(allocID, taskName string, canary bool) string { if len(s.Meta) > 0 { fmt.Fprintf(h, "%v", s.Meta) } + if len(s.CanaryMeta) > 0 { + fmt.Fprintf(h, "%v", s.CanaryMeta) + } // Vary ID on whether or not CanaryTags will be used if canary { @@ -526,6 +531,10 @@ OUTER: return false } + if !reflect.DeepEqual(s.CanaryMeta, o.CanaryMeta) { + return false + } + if !helper.CompareSliceSetString(s.Tags, o.Tags) { return false } diff --git a/website/source/docs/job-specification/service.html.md b/website/source/docs/job-specification/service.html.md index 4b2a59c01..7e616777d 100644 --- a/website/source/docs/job-specification/service.html.md +++ b/website/source/docs/job-specification/service.html.md @@ -128,7 +128,7 @@ Connect][connect] integration. this service when the service is part of an allocation that is currently a canary. Once the canary is promoted, the registered tags will be updated to those specified in the `tags` parameter. If this is not supplied, the - registered tags will be equal to that of the `tags parameter. + registered tags will be equal to that of the `tags` parameter. - `address_mode` `(string: "auto")` - Specifies what address (host or driver-specific) this service should advertise. This setting is supported in @@ -151,6 +151,13 @@ Connect][connect] integration. - `meta` ([Meta][]: nil) - Specifies a key-value map that annotates the Consul service with user-defined metadata. +- `canary_meta` ([Meta][]: nil) - Specifies a key-value map that + annotates the Consul service with user-defined metadata when the service is + part of an allocation that is currently a canary. Once the canary is + promoted, the registered meta will be updated to those specified in the + `meta` parameter. If this is not supploed, the registered meta will be set to + that of the `meta` parameter. + ### `check` Parameters Note that health checks run inside the task. If your task is a Docker container,