Add agent.segment interpolation to prepared queries

This commit is contained in:
Kyle Havlovitz 2017-08-29 17:02:50 -07:00
parent 107d7f6c5a
commit 1c04f1537a
No known key found for this signature in database
GPG Key ID: 8A5E6B173056AD6C
12 changed files with 74 additions and 46 deletions

View File

@ -357,8 +357,7 @@ type NetworkSegment struct {
Name string `mapstructure:"name"` Name string `mapstructure:"name"`
// Bind is the bind address for this segment. // Bind is the bind address for this segment.
Bind string `mapstructure:"bind"` Bind string `mapstructure:"bind"`
BindAddrs []string `mapstructure:"-"`
// Port is the port for this segment. // Port is the port for this segment.
Port int `mapstructure:"port"` Port int `mapstructure:"port"`
@ -1467,7 +1466,7 @@ func DecodeConfig(r io.Reader) (*Config, error) {
} }
// Validate node meta fields // Validate node meta fields
if err := structs.ValidateMetadata(result.Meta); err != nil { if err := structs.ValidateMetadata(result.Meta, false); err != nil {
return nil, fmt.Errorf("Failed to parse node metadata: %v", err) return nil, fmt.Errorf("Failed to parse node metadata: %v", err)
} }

View File

@ -597,11 +597,10 @@ func TestDecodeConfig(t *testing.T) {
c: &Config{Segment: "thing"}, c: &Config{Segment: "thing"},
}, },
{ {
in: `{"server": true, "segments":[{"name": "alpha", "bind": "127.0.0.1", "port": 1234, "rpc_listener": true, "advertise": "1.1.1.1"}]}`, in: `{"segments":[{"name": "alpha", "bind": "127.0.0.1", "port": 1234, "rpc_listener": true, "advertise": "1.1.1.1"}]}`,
c: &Config{Server: true, Segments: []NetworkSegment{{ c: &Config{Segments: []NetworkSegment{{
Name: "alpha", Name: "alpha",
Bind: "127.0.0.1", Bind: "127.0.0.1",
BindAddrs: []string{"127.0.0.1"},
Port: 1234, Port: 1234,
RPCListener: true, RPCListener: true,
Advertise: "1.1.1.1", Advertise: "1.1.1.1",

View File

@ -89,7 +89,7 @@ func Compile(query *structs.PreparedQuery) (*CompiledTemplate, error) {
// prefix it will be expected to run with. The results might not make // prefix it will be expected to run with. The results might not make
// sense and create a valid service to lookup, but it should render // sense and create a valid service to lookup, but it should render
// without any errors. // without any errors.
if _, err = ct.Render(ct.query.Name); err != nil { if _, err = ct.Render(ct.query.Name, structs.QuerySource{}); err != nil {
return nil, err return nil, err
} }
@ -99,7 +99,7 @@ func Compile(query *structs.PreparedQuery) (*CompiledTemplate, error) {
// Render takes a compiled template and renders it for the given name. For // Render takes a compiled template and renders it for the given name. For
// example, if the user looks up foobar.query.consul via DNS then we will call // example, if the user looks up foobar.query.consul via DNS then we will call
// this function with "foobar" on the compiled template. // this function with "foobar" on the compiled template.
func (ct *CompiledTemplate) Render(name string) (*structs.PreparedQuery, error) { func (ct *CompiledTemplate) Render(name string, source structs.QuerySource) (*structs.PreparedQuery, error) {
// Make it "safe" to render a default structure. // Make it "safe" to render a default structure.
if ct == nil { if ct == nil {
return nil, fmt.Errorf("Cannot render an uncompiled template") return nil, fmt.Errorf("Cannot render an uncompiled template")
@ -156,6 +156,10 @@ func (ct *CompiledTemplate) Render(name string) (*structs.PreparedQuery, error)
Type: ast.TypeString, Type: ast.TypeString,
Value: strings.TrimPrefix(name, query.Name), Value: strings.TrimPrefix(name, query.Name),
}, },
"agent.segment": ast.Variable{
Type: ast.TypeString,
Value: source.Segment,
},
}, },
FuncMap: map[string]ast.Function{ FuncMap: map[string]ast.Function{
"match": match, "match": match,

View File

@ -29,6 +29,7 @@ var (
"${match(0)}", "${match(0)}",
"${match(1)}", "${match(1)}",
"${match(2)}", "${match(2)}",
"${agent.segment}",
}, },
}, },
Tags: []string{ Tags: []string{
@ -38,11 +39,13 @@ var (
"${match(0)}", "${match(0)}",
"${match(1)}", "${match(1)}",
"${match(2)}", "${match(2)}",
"${agent.segment}",
}, },
NodeMeta: map[string]string{ NodeMeta: map[string]string{
"foo": "${name.prefix}", "foo": "${name.prefix}",
"bar": "${match(0)}", "bar": "${match(0)}",
"baz": "${match(1)}", "baz": "${match(1)}",
"zoo": "${agent.segment}",
}, },
}, },
} }
@ -83,7 +86,7 @@ func renderBench(b *testing.B, query *structs.PreparedQuery) {
} }
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
_, err := compiled.Render("hello-bench-mark") _, err := compiled.Render("hello-bench-mark", structs.QuerySource{})
if err != nil { if err != nil {
b.Fatalf("err: %v", err) b.Fatalf("err: %v", err)
} }
@ -121,7 +124,7 @@ func TestTemplate_Compile(t *testing.T) {
query.Template.Type = structs.QueryTemplateTypeNamePrefixMatch query.Template.Type = structs.QueryTemplateTypeNamePrefixMatch
query.Template.Regexp = "^(hello)there$" query.Template.Regexp = "^(hello)there$"
query.Service.Service = "${name.full}" query.Service.Service = "${name.full}"
query.Service.Tags = []string{"${match(1)}"} query.Service.Tags = []string{"${match(1)}", "${agent.segment}"}
backup, err := copystructure.Copy(query) backup, err := copystructure.Copy(query)
if err != nil { if err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
@ -135,7 +138,7 @@ func TestTemplate_Compile(t *testing.T) {
} }
// Do a sanity check render on it. // Do a sanity check render on it.
actual, err := ct.Render("hellothere") actual, err := ct.Render("hellothere", structs.QuerySource{Segment: "segment-foo"})
if err != nil { if err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
@ -150,6 +153,7 @@ func TestTemplate_Compile(t *testing.T) {
Service: "hellothere", Service: "hellothere",
Tags: []string{ Tags: []string{
"hello", "hello",
"segment-foo",
}, },
}, },
} }
@ -201,7 +205,7 @@ func TestTemplate_Render(t *testing.T) {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
actual, err := ct.Render("unused") actual, err := ct.Render("unused", structs.QuerySource{})
if err != nil { if err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
@ -218,7 +222,7 @@ func TestTemplate_Render(t *testing.T) {
Regexp: "^(.*?)-(.*?)-(.*)$", Regexp: "^(.*?)-(.*?)-(.*)$",
}, },
Service: structs.ServiceQuery{ Service: structs.ServiceQuery{
Service: "${name.prefix} xxx ${name.full} xxx ${name.suffix}", Service: "${name.prefix} xxx ${name.full} xxx ${name.suffix} xxx ${agent.segment}",
Tags: []string{ Tags: []string{
"${match(-1)}", "${match(-1)}",
"${match(0)}", "${match(0)}",
@ -238,7 +242,7 @@ func TestTemplate_Render(t *testing.T) {
// Run a case that matches the regexp. // Run a case that matches the regexp.
{ {
actual, err := ct.Render("hello-foo-bar-none") actual, err := ct.Render("hello-foo-bar-none", structs.QuerySource{Segment: "segment-bar"})
if err != nil { if err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
@ -249,7 +253,7 @@ func TestTemplate_Render(t *testing.T) {
Regexp: "^(.*?)-(.*?)-(.*)$", Regexp: "^(.*?)-(.*?)-(.*)$",
}, },
Service: structs.ServiceQuery{ Service: structs.ServiceQuery{
Service: "hello- xxx hello-foo-bar-none xxx foo-bar-none", Service: "hello- xxx hello-foo-bar-none xxx foo-bar-none xxx segment-bar",
Tags: []string{ Tags: []string{
"", "",
"hello-foo-bar-none", "hello-foo-bar-none",
@ -269,7 +273,7 @@ func TestTemplate_Render(t *testing.T) {
// Run a case that doesn't match the regexp // Run a case that doesn't match the regexp
{ {
actual, err := ct.Render("hello-nope") actual, err := ct.Render("hello-nope", structs.QuerySource{Segment: "segment-bar"})
if err != nil { if err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
@ -280,7 +284,7 @@ func TestTemplate_Render(t *testing.T) {
Regexp: "^(.*?)-(.*?)-(.*)$", Regexp: "^(.*?)-(.*?)-(.*)$",
}, },
Service: structs.ServiceQuery{ Service: structs.ServiceQuery{
Service: "hello- xxx hello-nope xxx nope", Service: "hello- xxx hello-nope xxx nope xxx segment-bar",
Tags: []string{ Tags: []string{
"", "",
"", "",
@ -307,7 +311,7 @@ func TestTemplate_Render(t *testing.T) {
RemoveEmptyTags: true, RemoveEmptyTags: true,
}, },
Service: structs.ServiceQuery{ Service: structs.ServiceQuery{
Service: "${name.prefix} xxx ${name.full} xxx ${name.suffix}", Service: "${name.prefix} xxx ${name.full} xxx ${name.suffix} xxx ${agent.segment}",
Tags: []string{ Tags: []string{
"${match(-1)}", "${match(-1)}",
"${match(0)}", "${match(0)}",
@ -326,7 +330,7 @@ func TestTemplate_Render(t *testing.T) {
// Run a case that matches the regexp, removing empty tags. // Run a case that matches the regexp, removing empty tags.
{ {
actual, err := ct.Render("hello-foo-bar-none") actual, err := ct.Render("hello-foo-bar-none", structs.QuerySource{Segment: "segment-baz"})
if err != nil { if err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
@ -338,7 +342,7 @@ func TestTemplate_Render(t *testing.T) {
RemoveEmptyTags: true, RemoveEmptyTags: true,
}, },
Service: structs.ServiceQuery{ Service: structs.ServiceQuery{
Service: "hello- xxx hello-foo-bar-none xxx foo-bar-none", Service: "hello- xxx hello-foo-bar-none xxx foo-bar-none xxx segment-baz",
Tags: []string{ Tags: []string{
"hello-foo-bar-none", "hello-foo-bar-none",
"hello", "hello",
@ -355,7 +359,7 @@ func TestTemplate_Render(t *testing.T) {
// Run a case that doesn't match the regexp, removing empty tags. // Run a case that doesn't match the regexp, removing empty tags.
{ {
actual, err := ct.Render("hello-nope") actual, err := ct.Render("hello-nope", structs.QuerySource{Segment: "segment-baz"})
if err != nil { if err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
@ -367,7 +371,7 @@ func TestTemplate_Render(t *testing.T) {
RemoveEmptyTags: true, RemoveEmptyTags: true,
}, },
Service: structs.ServiceQuery{ Service: structs.ServiceQuery{
Service: "hello- xxx hello-nope xxx nope", Service: "hello- xxx hello-nope xxx nope xxx segment-baz",
Tags: []string{ Tags: []string{
"42", "42",
}, },

View File

@ -182,7 +182,7 @@ func parseService(svc *structs.ServiceQuery) error {
} }
// Make sure the metadata filters are valid // Make sure the metadata filters are valid
if err := structs.ValidateMetadata(svc.NodeMeta); err != nil { if err := structs.ValidateMetadata(svc.NodeMeta, true); err != nil {
return err return err
} }
@ -298,7 +298,7 @@ func (p *PreparedQuery) Explain(args *structs.PreparedQueryExecuteRequest,
// Try to locate the query. // Try to locate the query.
state := p.srv.fsm.State() state := p.srv.fsm.State()
_, query, err := state.PreparedQueryResolve(args.QueryIDOrName) _, query, err := state.PreparedQueryResolve(args.QueryIDOrName, args.Agent)
if err != nil { if err != nil {
return err return err
} }
@ -345,7 +345,7 @@ func (p *PreparedQuery) Execute(args *structs.PreparedQueryExecuteRequest,
// Try to locate the query. // Try to locate the query.
state := p.srv.fsm.State() state := p.srv.fsm.State()
_, query, err := state.PreparedQueryResolve(args.QueryIDOrName) _, query, err := state.PreparedQueryResolve(args.QueryIDOrName, args.Agent)
if err != nil { if err != nil {
return err return err
} }

View File

@ -256,7 +256,7 @@ func (s *Store) PreparedQueryGet(ws memdb.WatchSet, queryID string) (uint64, *st
// PreparedQueryResolve returns the given prepared query by looking up an ID or // PreparedQueryResolve returns the given prepared query by looking up an ID or
// Name. If the query was looked up by name and it's a template, then the // Name. If the query was looked up by name and it's a template, then the
// template will be rendered before it is returned. // template will be rendered before it is returned.
func (s *Store) PreparedQueryResolve(queryIDOrName string) (uint64, *structs.PreparedQuery, error) { func (s *Store) PreparedQueryResolve(queryIDOrName string, source structs.QuerySource) (uint64, *structs.PreparedQuery, error) {
tx := s.db.Txn(false) tx := s.db.Txn(false)
defer tx.Abort() defer tx.Abort()
@ -293,7 +293,7 @@ func (s *Store) PreparedQueryResolve(queryIDOrName string) (uint64, *structs.Pre
prep := func(wrapped interface{}) (uint64, *structs.PreparedQuery, error) { prep := func(wrapped interface{}) (uint64, *structs.PreparedQuery, error) {
wrapper := wrapped.(*queryWrapper) wrapper := wrapped.(*queryWrapper)
if prepared_query.IsTemplate(wrapper.PreparedQuery) { if prepared_query.IsTemplate(wrapper.PreparedQuery) {
render, err := wrapper.ct.Render(queryIDOrName) render, err := wrapper.ct.Render(queryIDOrName, source)
if err != nil { if err != nil {
return idx, nil, err return idx, nil, err
} }

View File

@ -554,7 +554,7 @@ func TestStateStore_PreparedQueryResolve(t *testing.T) {
// Try to lookup a query that's not there using something that looks // Try to lookup a query that's not there using something that looks
// like a real ID. // like a real ID.
idx, actual, err := s.PreparedQueryResolve(query.ID) idx, actual, err := s.PreparedQueryResolve(query.ID, structs.QuerySource{})
if err != nil { if err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }
@ -567,7 +567,7 @@ func TestStateStore_PreparedQueryResolve(t *testing.T) {
// Try to lookup a query that's not there using something that looks // Try to lookup a query that's not there using something that looks
// like a name // like a name
idx, actual, err = s.PreparedQueryResolve(query.Name) idx, actual, err = s.PreparedQueryResolve(query.Name, structs.QuerySource{})
if err != nil { if err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }
@ -600,7 +600,7 @@ func TestStateStore_PreparedQueryResolve(t *testing.T) {
ModifyIndex: 3, ModifyIndex: 3,
}, },
} }
idx, actual, err = s.PreparedQueryResolve(query.ID) idx, actual, err = s.PreparedQueryResolve(query.ID, structs.QuerySource{})
if err != nil { if err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }
@ -612,7 +612,7 @@ func TestStateStore_PreparedQueryResolve(t *testing.T) {
} }
// Read it back using the name and verify it again. // Read it back using the name and verify it again.
idx, actual, err = s.PreparedQueryResolve(query.Name) idx, actual, err = s.PreparedQueryResolve(query.Name, structs.QuerySource{})
if err != nil { if err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }
@ -625,7 +625,7 @@ func TestStateStore_PreparedQueryResolve(t *testing.T) {
// Make sure an empty lookup is well-behaved if there are actual queries // Make sure an empty lookup is well-behaved if there are actual queries
// in the state store. // in the state store.
idx, actual, err = s.PreparedQueryResolve("") idx, actual, err = s.PreparedQueryResolve("", structs.QuerySource{})
if err != ErrMissingQueryID { if err != ErrMissingQueryID {
t.Fatalf("bad: %v ", err) t.Fatalf("bad: %v ", err)
} }
@ -681,7 +681,7 @@ func TestStateStore_PreparedQueryResolve(t *testing.T) {
ModifyIndex: 4, ModifyIndex: 4,
}, },
} }
idx, actual, err = s.PreparedQueryResolve("prod-mongodb") idx, actual, err = s.PreparedQueryResolve("prod-mongodb", structs.QuerySource{})
if err != nil { if err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }
@ -708,7 +708,7 @@ func TestStateStore_PreparedQueryResolve(t *testing.T) {
ModifyIndex: 5, ModifyIndex: 5,
}, },
} }
idx, actual, err = s.PreparedQueryResolve("prod-redis-foobar") idx, actual, err = s.PreparedQueryResolve("prod-redis-foobar", structs.QuerySource{})
if err != nil { if err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }
@ -735,7 +735,7 @@ func TestStateStore_PreparedQueryResolve(t *testing.T) {
ModifyIndex: 4, ModifyIndex: 4,
}, },
} }
idx, actual, err = s.PreparedQueryResolve("prod-") idx, actual, err = s.PreparedQueryResolve("prod-", structs.QuerySource{})
if err != nil { if err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }
@ -748,7 +748,7 @@ func TestStateStore_PreparedQueryResolve(t *testing.T) {
// Make sure you can't run a prepared query template by ID, since that // Make sure you can't run a prepared query template by ID, since that
// makes no sense. // makes no sense.
_, _, err = s.PreparedQueryResolve(tmpl1.ID) _, _, err = s.PreparedQueryResolve(tmpl1.ID, structs.QuerySource{})
if err == nil || !strings.Contains(err.Error(), "prepared query templates can only be resolved up by name") { if err == nil || !strings.Contains(err.Error(), "prepared query templates can only be resolved up by name") {
t.Fatalf("bad: %v", err) t.Fatalf("bad: %v", err)
} }
@ -960,7 +960,7 @@ func TestStateStore_PreparedQuery_Snapshot_Restore(t *testing.T) {
// Make sure the second query, which is a template, was compiled // Make sure the second query, which is a template, was compiled
// and can be resolved. // and can be resolved.
_, query, err := s.PreparedQueryResolve("bob-backwards-is-bob") _, query, err := s.PreparedQueryResolve("bob-backwards-is-bob", structs.QuerySource{})
if err != nil { if err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }

View File

@ -96,6 +96,7 @@ func (s *HTTPServer) preparedQueryExecute(id string, resp http.ResponseWriter, r
Agent: structs.QuerySource{ Agent: structs.QuerySource{
Node: s.agent.config.NodeName, Node: s.agent.config.NodeName,
Datacenter: s.agent.config.Datacenter, Datacenter: s.agent.config.Datacenter,
Segment: s.agent.config.Segment,
}, },
} }
s.parseSource(req, &args.Source) s.parseSource(req, &args.Source)
@ -140,6 +141,7 @@ func (s *HTTPServer) preparedQueryExplain(id string, resp http.ResponseWriter, r
Agent: structs.QuerySource{ Agent: structs.QuerySource{
Node: s.agent.config.NodeName, Node: s.agent.config.NodeName,
Datacenter: s.agent.config.Datacenter, Datacenter: s.agent.config.Datacenter,
Segment: s.agent.config.Segment,
}, },
} }
s.parseSource(req, &args.Source) s.parseSource(req, &args.Source)

View File

@ -242,6 +242,7 @@ func (r *DeregisterRequest) RequestDatacenter() string {
// coordinates. // coordinates.
type QuerySource struct { type QuerySource struct {
Datacenter string Datacenter string
Segment string
Node string Node string
} }
@ -310,13 +311,13 @@ type Node struct {
type Nodes []*Node type Nodes []*Node
// ValidateMeta validates a set of key/value pairs from the agent config // ValidateMeta validates a set of key/value pairs from the agent config
func ValidateMetadata(meta map[string]string) error { func ValidateMetadata(meta map[string]string, allowConsulPrefix bool) error {
if len(meta) > metaMaxKeyPairs { if len(meta) > metaMaxKeyPairs {
return fmt.Errorf("Node metadata cannot contain more than %d key/value pairs", metaMaxKeyPairs) return fmt.Errorf("Node metadata cannot contain more than %d key/value pairs", metaMaxKeyPairs)
} }
for key, value := range meta { for key, value := range meta {
if err := validateMetaPair(key, value); err != nil { if err := validateMetaPair(key, value, allowConsulPrefix); err != nil {
return fmt.Errorf("Couldn't load metadata pair ('%s', '%s'): %s", key, value, err) return fmt.Errorf("Couldn't load metadata pair ('%s', '%s'): %s", key, value, err)
} }
} }
@ -325,7 +326,7 @@ func ValidateMetadata(meta map[string]string) error {
} }
// validateMetaPair checks that the given key/value pair is in a valid format // validateMetaPair checks that the given key/value pair is in a valid format
func validateMetaPair(key, value string) error { func validateMetaPair(key, value string, allowConsulPrefix bool) error {
if key == "" { if key == "" {
return fmt.Errorf("Key cannot be blank") return fmt.Errorf("Key cannot be blank")
} }
@ -335,7 +336,7 @@ func validateMetaPair(key, value string) error {
if len(key) > metaKeyMaxLength { if len(key) > metaKeyMaxLength {
return fmt.Errorf("Key is too long (limit: %d characters)", metaKeyMaxLength) return fmt.Errorf("Key is too long (limit: %d characters)", metaKeyMaxLength)
} }
if strings.HasPrefix(key, metaKeyReservedPrefix) { if strings.HasPrefix(key, metaKeyReservedPrefix) && !allowConsulPrefix {
return fmt.Errorf("Key prefix '%s' is reserved for internal use", metaKeyReservedPrefix) return fmt.Errorf("Key prefix '%s' is reserved for internal use", metaKeyReservedPrefix)
} }
if len(value) > metaValueMaxLength { if len(value) > metaValueMaxLength {

View File

@ -482,7 +482,7 @@ func TestStructs_ValidateMetadata(t *testing.T) {
"key2": "value2", "key2": "value2",
} }
// Should succeed // Should succeed
if err := ValidateMetadata(meta); err != nil { if err := ValidateMetadata(meta, false); err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }
@ -490,7 +490,7 @@ func TestStructs_ValidateMetadata(t *testing.T) {
meta = map[string]string{ meta = map[string]string{
"": "value1", "": "value1",
} }
if err := ValidateMetadata(meta); !strings.Contains(err.Error(), "Couldn't load metadata pair") { if err := ValidateMetadata(meta, false); !strings.Contains(err.Error(), "Couldn't load metadata pair") {
t.Fatalf("should have failed") t.Fatalf("should have failed")
} }
@ -499,7 +499,7 @@ func TestStructs_ValidateMetadata(t *testing.T) {
for i := 0; i < metaMaxKeyPairs+1; i++ { for i := 0; i < metaMaxKeyPairs+1; i++ {
meta[string(i)] = "value" meta[string(i)] = "value"
} }
if err := ValidateMetadata(meta); !strings.Contains(err.Error(), "cannot contain more than") { if err := ValidateMetadata(meta, false); !strings.Contains(err.Error(), "cannot contain more than") {
t.Fatalf("should have failed") t.Fatalf("should have failed")
} }
} }
@ -529,7 +529,7 @@ func TestStructs_validateMetaPair(t *testing.T) {
} }
for _, pair := range pairs { for _, pair := range pairs {
err := validateMetaPair(pair.Key, pair.Value) err := validateMetaPair(pair.Key, pair.Value, false)
if pair.Error == "" && err != nil { if pair.Error == "" && err != nil {
t.Fatalf("should have succeeded: %v, %v", pair, err) t.Fatalf("should have succeeded: %v, %v", pair, err)
} else if pair.Error != "" && !strings.Contains(err.Error(), pair.Error) { } else if pair.Error != "" && !strings.Contains(err.Error(), pair.Error) {

View File

@ -225,7 +225,7 @@ func (cmd *AgentCommand) readConfig() *agent.Config {
key, value := agent.ParseMetaPair(entry) key, value := agent.ParseMetaPair(entry)
cmdCfg.Meta[key] = value cmdCfg.Meta[key] = value
} }
if err := structs.ValidateMetadata(cmdCfg.Meta); err != nil { if err := structs.ValidateMetadata(cmdCfg.Meta, false); err != nil {
cmd.UI.Error(fmt.Sprintf("Failed to parse node metadata: %v", err)) cmd.UI.Error(fmt.Sprintf("Failed to parse node metadata: %v", err))
return nil return nil
} }

View File

@ -87,6 +87,25 @@ populate the query before it is executed. All of the string fields inside the
doesn't match, or an invalid index is given, then `${match(N)}` will return an doesn't match, or an invalid index is given, then `${match(N)}` will return an
empty string. empty string.
- `${agent.segment}` has the network segment (Enterprise only) of the agent that
initiated the query. This can be used with the `NodeMeta` field to limit the results
of a query to service instances within its own network segment:
```json
{
"Name": "",
"Template": {
"Type": "name_prefix_match"
},
"Service": {
"Service": "${name.full}",
"NodeMeta": {"consul-network-segment": "${agent.segment}"}
}
}
```
This will map all names of the form "&lt;service&gt;.query.consul" over DNS to a query
that will select an instance of the service in the agent's own network segment.
Using templates, it is possible to apply prepared query behaviors to many Using templates, it is possible to apply prepared query behaviors to many
services with a single template. Here's an example template that matches any services with a single template. Here's an example template that matches any
query and applies a failover policy to it: query and applies a failover policy to it: