From 32e821eda287848e6283543eff3d39bb8e446f94 Mon Sep 17 00:00:00 2001 From: Matt Keeler Date: Mon, 29 Apr 2019 15:28:01 -0400 Subject: [PATCH] Enabling "service" watch handler to accept a slice of tags Originally from PR #5347 --- api/watch/funcs.go | 10 ++- api/watch/funcs_test.go | 95 +++++++++++++++++++++++ api/watch/watch.go | 31 ++++++++ command/watch/watch.go | 8 +- website/source/docs/agent/watches.html.md | 38 +++++++-- 5 files changed, 166 insertions(+), 16 deletions(-) diff --git a/api/watch/funcs.go b/api/watch/funcs.go index 3ff5e5309..e9b2d8115 100644 --- a/api/watch/funcs.go +++ b/api/watch/funcs.go @@ -134,15 +134,17 @@ func serviceWatch(params map[string]interface{}) (WatcherFunc, error) { return nil, err } - var service, tag string + var ( + service string + tags []string + ) if err := assignValue(params, "service", &service); err != nil { return nil, err } if service == "" { return nil, fmt.Errorf("Must specify a single service to watch") } - - if err := assignValue(params, "tag", &tag); err != nil { + if err := assignValueStringSlice(params, "tag", &tags); err != nil { return nil, err } @@ -155,7 +157,7 @@ func serviceWatch(params map[string]interface{}) (WatcherFunc, error) { health := p.client.Health() opts := makeQueryOptionsWithContext(p, stale) defer p.cancelFunc() - nodes, meta, err := health.Service(service, tag, passingOnly, &opts) + nodes, meta, err := health.ServiceMultipleTags(service, tags, passingOnly, &opts) if err != nil { return nil, nil, err } diff --git a/api/watch/funcs_test.go b/api/watch/funcs_test.go index c22ae1ecc..be70f591c 100644 --- a/api/watch/funcs_test.go +++ b/api/watch/funcs_test.go @@ -428,6 +428,101 @@ func TestServiceWatch(t *testing.T) { wg.Wait() } +func TestServiceMultipleTagsWatch(t *testing.T) { + t.Parallel() + c, s := makeClient(t) + defer s.Stop() + + invoke := makeInvokeCh() + plan := mustParse(t, `{"type":"service", "service":"foo", "tag":["bar","buzz"], "passingonly":true}`) + plan.Handler = func(idx uint64, raw interface{}) { + if raw == nil { + return // ignore + } + v, ok := raw.([]*api.ServiceEntry) + if !ok || len(v) == 0 { + return // ignore + } + if v[0].Service.ID != "foobarbuzzbiff" { + invoke <- errBadContent + return + } + if len(v[0].Service.Tags) == 0 { + invoke <- errBadContent + return + } + // test for our tags + barFound := false + buzzFound := false + for _, t := range v[0].Service.Tags { + if t == "bar" { + barFound = true + } else if t == "buzz" { + buzzFound = true + } + } + if !barFound || !buzzFound { + invoke <- errBadContent + return + } + invoke <- nil + } + + var wg sync.WaitGroup + + wg.Add(1) + go func() { + defer wg.Done() + agent := c.Agent() + + // we do not want to find this one. + time.Sleep(20 * time.Millisecond) + reg := &api.AgentServiceRegistration{ + ID: "foobarbiff", + Name: "foo", + Tags: []string{"bar", "biff"}, + } + if err := agent.ServiceRegister(reg); err != nil { + t.Fatalf("err: %v", err) + } + + // we do not want to find this one. + reg = &api.AgentServiceRegistration{ + ID: "foobuzzbiff", + Name: "foo", + Tags: []string{"buzz", "biff"}, + } + if err := agent.ServiceRegister(reg); err != nil { + t.Fatalf("err: %v", err) + } + + // we want to find this one + reg = &api.AgentServiceRegistration{ + ID: "foobarbuzzbiff", + Name: "foo", + Tags: []string{"bar", "buzz", "biff"}, + } + if err := agent.ServiceRegister(reg); err != nil { + t.Fatalf("err: %v", err) + } + }() + + wg.Add(1) + go func() { + defer wg.Done() + if err := plan.Run(s.HTTPAddr); err != nil { + t.Fatalf("err: %v", err) + } + }() + + if err := <-invoke; err != nil { + t.Fatalf("err: %v", err) + } + + plan.Stop() + wg.Wait() +} + func TestChecksWatch_State(t *testing.T) { t.Parallel() c, s := makeClient(t) diff --git a/api/watch/watch.go b/api/watch/watch.go index 346105e8f..3690a20c9 100644 --- a/api/watch/watch.go +++ b/api/watch/watch.go @@ -233,6 +233,37 @@ func assignValueBool(params map[string]interface{}, name string, out *bool) erro return nil } +// assignValueStringSlice is used to extract a value ensuring it is either a string or a slice of strings +func assignValueStringSlice(params map[string]interface{}, name string, out *[]string) error { + if raw, ok := params[name]; ok { + var tmp []string + switch raw.(type) { + case string: + tmp = make([]string, 1, 1) + tmp[0] = raw.(string) + case []string: + l := len(raw.([]string)) + tmp = make([]string, l, l) + copy(tmp, raw.([]string)) + case []interface{}: + l := len(raw.([]interface{})) + tmp = make([]string, l, l) + for i, v := range raw.([]interface{}) { + if s, ok := v.(string); ok { + tmp[i] = s + } else { + return fmt.Errorf("Index %d of %s expected to be string", i, name) + } + } + default: + return fmt.Errorf("Expecting %s to be a string or []string", name) + } + *out = tmp + delete(params, name) + } + return nil +} + // Parse the 'http_handler_config' parameters func parseHttpHandlerConfig(configParams interface{}) (*HttpHandlerConfig, error) { var config HttpHandlerConfig diff --git a/command/watch/watch.go b/command/watch/watch.go index 92178a1f0..6a17b8309 100644 --- a/command/watch/watch.go +++ b/command/watch/watch.go @@ -36,7 +36,7 @@ type cmd struct { key string prefix string service string - tag string + tag []string passingOnly string state string name string @@ -55,8 +55,8 @@ func (c *cmd) init() { c.flags.StringVar(&c.service, "service", "", "Specifies the service to watch. Required for 'service' type, "+ "optional for 'checks' type.") - c.flags.StringVar(&c.tag, "tag", "", - "Specifies the service tag to filter on. Optional for 'service' type.") + c.flags.Var((*flags.AppendSliceValue)(&c.tag), "tag", "Specifies the service tag(s) to filter on. "+ + "Optional for 'service' type. May be specified multiple times") c.flags.StringVar(&c.passingOnly, "passingonly", "", "Specifies if only hosts passing all checks are displayed. "+ "Optional for 'service' type, must be one of `[true|false]`. Defaults false.") @@ -115,7 +115,7 @@ func (c *cmd) Run(args []string) int { if c.service != "" { params["service"] = c.service } - if c.tag != "" { + if len(c.tag) > 0 { params["tag"] = c.tag } if c.http.Stale() { diff --git a/website/source/docs/agent/watches.html.md b/website/source/docs/agent/watches.html.md index 8669cdcf7..2cc02cfe6 100644 --- a/website/source/docs/agent/watches.html.md +++ b/website/source/docs/agent/watches.html.md @@ -266,26 +266,45 @@ An example of the output of this command: The "service" watch type is used to monitor the providers of a single service. It requires the "service" parameter -and optionally takes the parameters "tag" and "passingonly". -The "tag" parameter will filter by tag, and "passingonly" is -a boolean that will filter to only the instances passing all -health checks. +and optionally takes the parameters "tag" and +"passingonly". The "tag" parameter will filter by one or more tags. +It may be either a single string value or a slice of strings. +The "passingonly" is a boolean that will filter to only the +instances passing all health checks. This maps to the `/v1/health/service` API internally. -Here is an example configuration: +Here is an example configuration with a single tag: ```javascript { "type": "service", "service": "redis", - "args": ["/usr/bin/my-service-handler.sh", "-redis"] + "args": ["/usr/bin/my-service-handler.sh", "-redis"], + "tag": "bar" +} +``` + +Here is an example configuration with multiple tags: + +```javascript +{ + "type": "service", + "service": "redis", + "args": ["/usr/bin/my-service-handler.sh", "-redis"], + "tag": ["bar", "foo"] } ``` Or, using the watch command: - $ consul watch -type=service -service=redis /usr/bin/my-service-handler.sh +Single tag: + + $ consul watch -type=service -service=redis -tag=bar /usr/bin/my-service-handler.sh + +Multiple tag: + + $ consul watch -type=service -service=redis -tag=bar -tag=foo /usr/bin/my-service-handler.sh An example of the output of this command: @@ -299,7 +318,10 @@ An example of the output of this command: "Service": { "ID": "redis", "Service": "redis", - "Tags": null, + "Tags": [ + "bar", + "foo" + ], "Port": 8000 }, "Checks": [