diff --git a/agent/agent_endpoint.go b/agent/agent_endpoint.go index 0e2b4b48033c..0fa02b973889 100644 --- a/agent/agent_endpoint.go +++ b/agent/agent_endpoint.go @@ -131,9 +131,18 @@ func (s *HTTPServer) AgentServices(resp http.ResponseWriter, req *http.Request) // Use empty list instead of nil for id, s := range services { - if s.Tags == nil { + if s.Tags == nil || s.Meta == nil { clone := *s - clone.Tags = make([]string, 0) + if s.Tags == nil { + clone.Tags = make([]string, 0) + } else { + clone.Tags = s.Tags + } + if s.Meta == nil { + clone.Meta = make(map[string]string) + } else { + clone.Meta = s.Meta + } services[id] = &clone } } diff --git a/agent/agent_endpoint_test.go b/agent/agent_endpoint_test.go index 940304b54892..c6e4f0532484 100644 --- a/agent/agent_endpoint_test.go +++ b/agent/agent_endpoint_test.go @@ -1204,6 +1204,7 @@ func TestAgent_RegisterService(t *testing.T) { args := &structs.ServiceDefinition{ Name: "test", + Meta: map[string]string{"hello": "world"}, Tags: []string{"master"}, Port: 8000, Check: structs.CheckType{ @@ -1232,6 +1233,9 @@ func TestAgent_RegisterService(t *testing.T) { if _, ok := a.State.Services()["test"]; !ok { t.Fatalf("missing test service") } + if val := a.State.Service("test").Meta["hello"]; val != "world" { + t.Fatalf("Missing meta: %v", a.State.Service("test").Meta) + } // Ensure we have a check mapping checks := a.State.Checks() @@ -1254,7 +1258,7 @@ func TestAgent_RegisterService_TranslateKeys(t *testing.T) { a := NewTestAgent(t.Name(), "") defer a.Shutdown() - json := `{"name":"test", "port":8000, "enable_tag_override": true}` + json := `{"name":"test", "port":8000, "enable_tag_override": true, "meta": {"some": "meta"}}` req, _ := http.NewRequest("PUT", "/v1/agent/service/register", strings.NewReader(json)) obj, err := a.srv.AgentRegisterService(nil, req) @@ -1264,10 +1268,10 @@ func TestAgent_RegisterService_TranslateKeys(t *testing.T) { if obj != nil { t.Fatalf("bad: %v", obj) } - svc := &structs.NodeService{ ID: "test", Service: "test", + Meta: map[string]string{"some": "meta"}, Port: 8000, EnableTagOverride: true, } diff --git a/agent/config/builder.go b/agent/config/builder.go index e9cb19394c65..c3fa42d1a4cb 100644 --- a/agent/config/builder.go +++ b/agent/config/builder.go @@ -997,11 +997,18 @@ func (b *Builder) serviceVal(v *ServiceDefinition) *structs.ServiceDefinition { checks = append(checks, b.checkVal(v.Check).CheckType()) } + meta := make(map[string]string) + if err := structs.ValidateMetadata(v.Meta, false); err != nil { + b.err = multierror.Append(fmt.Errorf("invalid meta for service %s: %v", b.stringVal(v.Name), err)) + } else { + meta = v.Meta + } return &structs.ServiceDefinition{ ID: b.stringVal(v.ID), Name: b.stringVal(v.Name), Tags: v.Tags, Address: b.stringVal(v.Address), + Meta: meta, Port: b.intVal(v.Port), Token: b.stringVal(v.Token), EnableTagOverride: b.boolVal(v.EnableTagOverride), diff --git a/agent/config/config.go b/agent/config/config.go index 2a14abc6b8ce..7ae14fcdd0b4 100644 --- a/agent/config/config.go +++ b/agent/config/config.go @@ -319,6 +319,7 @@ type ServiceDefinition struct { Name *string `json:"name,omitempty" hcl:"name" mapstructure:"name"` Tags []string `json:"tags,omitempty" hcl:"tags" mapstructure:"tags"` Address *string `json:"address,omitempty" hcl:"address" mapstructure:"address"` + Meta map[string]string `json:"meta,omitempty" hcl:"meta" mapstructure:"meta"` Port *int `json:"port,omitempty" hcl:"port" mapstructure:"port"` Check *CheckDefinition `json:"check,omitempty" hcl:"check" mapstructure:"check"` Checks []CheckDefinition `json:"checks,omitempty" hcl:"checks" mapstructure:"checks"` diff --git a/agent/config/runtime_test.go b/agent/config/runtime_test.go index da62e0339ca8..d48d288163a4 100644 --- a/agent/config/runtime_test.go +++ b/agent/config/runtime_test.go @@ -1923,20 +1923,59 @@ func TestConfigFlagsAndEdgecases(t *testing.T) { }, json: []string{ `{ "service": { "name": "a", "port": 80 } }`, - `{ "service": { "name": "b", "port": 90 } }`, + `{ "service": { "name": "b", "port": 90, "meta": {"my": "value"} } }`, }, hcl: []string{ `service = { name = "a" port = 80 }`, - `service = { name = "b" port = 90 }`, + `service = { name = "b" port = 90 meta={my="value"}}`, }, patch: func(rt *RuntimeConfig) { rt.Services = []*structs.ServiceDefinition{ &structs.ServiceDefinition{Name: "a", Port: 80}, - &structs.ServiceDefinition{Name: "b", Port: 90}, + &structs.ServiceDefinition{Name: "b", Port: 90, Meta: map[string]string{"my": "value"}}, } rt.DataDir = dataDir }, }, + { + desc: "service with wrong meta: too long key", + args: []string{ + `-data-dir=` + dataDir, + }, + json: []string{ + `{ "service": { "name": "a", "port": 80, "meta": { "` + randomString(520) + `": "metaValue" } } }`, + }, + hcl: []string{ + `service = { name = "a" port = 80, meta={` + randomString(520) + `="metaValue"} }`, + }, + err: `Key is too long`, + }, + { + desc: "service with wrong meta: too long value", + args: []string{ + `-data-dir=` + dataDir, + }, + json: []string{ + `{ "service": { "name": "a", "port": 80, "meta": { "a": "` + randomString(520) + `" } } }`, + }, + hcl: []string{ + `service = { name = "a" port = 80, meta={a="` + randomString(520) + `"} }`, + }, + err: `Value is too long`, + }, + { + desc: "service with wrong meta: too many meta", + args: []string{ + `-data-dir=` + dataDir, + }, + json: []string{ + `{ "service": { "name": "a", "port": 80, "meta": { ` + metaPairs(70, "json") + `} } }`, + }, + hcl: []string{ + `service = { name = "a" port = 80 meta={` + metaPairs(70, "hcl") + `} }`, + }, + err: `invalid meta for service a: Node metadata cannot contain more than 64 key`, + }, { desc: "translated keys", args: []string{ @@ -2397,6 +2436,9 @@ func TestFullConfig(t *testing.T) { "service": { "id": "dLOXpSCI", "name": "o1ynPkp0", + "meta": { + "mymeta": "data" + }, "tags": ["nkwshvM5", "NTDWn3ek"], "address": "cOlSOhbp", "token": "msy7iWER", @@ -2834,6 +2876,9 @@ func TestFullConfig(t *testing.T) { service = { id = "dLOXpSCI" name = "o1ynPkp0" + meta = { + mymeta = "data" + } tags = ["nkwshvM5", "NTDWn3ek"] address = "cOlSOhbp" token = "msy7iWER" @@ -3487,6 +3532,7 @@ func TestFullConfig(t *testing.T) { Tags: []string{"nkwshvM5", "NTDWn3ek"}, Address: "cOlSOhbp", Token: "msy7iWER", + Meta: map[string]string{"mymeta": "data"}, Port: 24237, EnableTagOverride: true, Checks: structs.CheckTypes{ diff --git a/agent/consul/acl.go b/agent/consul/acl.go index bea2a5617db3..eb14e1dabcb3 100644 --- a/agent/consul/acl.go +++ b/agent/consul/acl.go @@ -678,6 +678,7 @@ func vetRegisterWithACL(rule acl.ACL, subj *structs.RegisterRequest, ID: subj.Service.ID, Service: subj.Service.Service, Tags: subj.Service.Tags, + Meta: subj.Service.Meta, Address: subj.Service.Address, Port: subj.Service.Port, EnableTagOverride: subj.Service.EnableTagOverride, diff --git a/api/agent.go b/api/agent.go index b42baed41d33..772948ad1ee5 100644 --- a/api/agent.go +++ b/api/agent.go @@ -23,6 +23,7 @@ type AgentService struct { ID string Service string Tags []string + Meta map[string]string Port int Address string EnableTagOverride bool diff --git a/api/agent_test.go b/api/agent_test.go index e5ccdb0ef253..a05c4faf1ae7 100644 --- a/api/agent_test.go +++ b/api/agent_test.go @@ -73,7 +73,7 @@ func TestAPI_AgentReload(t *testing.T) { agent := c.Agent() // Update the config file with a service definition - config := `{"service":{"name":"redis", "port":1234}}` + config := `{"service":{"name":"redis", "port":1234, "Meta": {"some": "meta"}}}` err = ioutil.WriteFile(configFile.Name(), []byte(config), 0644) if err != nil { t.Fatalf("err: %v", err) @@ -95,6 +95,9 @@ func TestAPI_AgentReload(t *testing.T) { if service.Port != 1234 { t.Fatalf("bad: %v", service.Port) } + if service.Meta["some"] != "meta" { + t.Fatalf("Missing metadata some:=meta in %v", service) + } } func TestAPI_AgentMembersOpts(t *testing.T) { diff --git a/website/source/docs/agent/services.html.md b/website/source/docs/agent/services.html.md index f8875e2bc1d8..d01b85a0bf82 100644 --- a/website/source/docs/agent/services.html.md +++ b/website/source/docs/agent/services.html.md @@ -25,6 +25,9 @@ A service definition is a script that looks like: "name": "redis", "tags": ["primary"], "address": "", + "meta": { + "meta": "for my service" + } "port": 8000, "enable_tag_override": false, "checks": [ @@ -38,8 +41,8 @@ A service definition is a script that looks like: ``` A service definition must include a `name` and may optionally provide an -`id`, `tags`, `address`, `port`, `check`, and `enable_tag_override`. The -`id` is set to the `name` if not provided. It is required that all +`id`, `tags`, `address`, `port`, `check`, `meta` and `enable_tag_override`. +The `id` is set to the `name` if not provided. It is required that all services have a unique ID per node, so if names might conflict then unique IDs should be provided. @@ -57,6 +60,14 @@ The `port` field can be used as well to make a service-oriented architecture simpler to configure; this way, the address and port of a service can be discovered. +The `meta` object is a map of max 64 key/values with string semantics. Key can contain +only ASCII chars and no special characters (`A-Z` `a-z` `0-9` `_` and `-`). +For performance and security reasons, values as well as keys are limited to 128 +characters for keys, 512 for values. This object has the same limitations as the node +meta object in node definition. +All those meta data can be retrieved individually per instance of the service +and all the instances of a given service have their own copy of it. + Services may also contain a `token` field to provide an ACL token. This token is used for any interaction with the catalog for the service, including [anti-entropy syncs](/docs/internals/anti-entropy.html) and deregistration.