Enabling "service" watch handler to accept a slice of tags

Originally from PR #5347
This commit is contained in:
Matt Keeler 2019-04-29 15:28:01 -04:00 committed by GitHub
parent 26ef7943ed
commit 32e821eda2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 166 additions and 16 deletions

View File

@ -134,15 +134,17 @@ func serviceWatch(params map[string]interface{}) (WatcherFunc, error) {
return nil, err return nil, err
} }
var service, tag string var (
service string
tags []string
)
if err := assignValue(params, "service", &service); err != nil { if err := assignValue(params, "service", &service); err != nil {
return nil, err return nil, err
} }
if service == "" { if service == "" {
return nil, fmt.Errorf("Must specify a single service to watch") return nil, fmt.Errorf("Must specify a single service to watch")
} }
if err := assignValueStringSlice(params, "tag", &tags); err != nil {
if err := assignValue(params, "tag", &tag); err != nil {
return nil, err return nil, err
} }
@ -155,7 +157,7 @@ func serviceWatch(params map[string]interface{}) (WatcherFunc, error) {
health := p.client.Health() health := p.client.Health()
opts := makeQueryOptionsWithContext(p, stale) opts := makeQueryOptionsWithContext(p, stale)
defer p.cancelFunc() defer p.cancelFunc()
nodes, meta, err := health.Service(service, tag, passingOnly, &opts) nodes, meta, err := health.ServiceMultipleTags(service, tags, passingOnly, &opts)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }

View File

@ -428,6 +428,101 @@ func TestServiceWatch(t *testing.T) {
wg.Wait() 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) { func TestChecksWatch_State(t *testing.T) {
t.Parallel() t.Parallel()
c, s := makeClient(t) c, s := makeClient(t)

View File

@ -233,6 +233,37 @@ func assignValueBool(params map[string]interface{}, name string, out *bool) erro
return nil 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 // Parse the 'http_handler_config' parameters
func parseHttpHandlerConfig(configParams interface{}) (*HttpHandlerConfig, error) { func parseHttpHandlerConfig(configParams interface{}) (*HttpHandlerConfig, error) {
var config HttpHandlerConfig var config HttpHandlerConfig

View File

@ -36,7 +36,7 @@ type cmd struct {
key string key string
prefix string prefix string
service string service string
tag string tag []string
passingOnly string passingOnly string
state string state string
name string name string
@ -55,8 +55,8 @@ func (c *cmd) init() {
c.flags.StringVar(&c.service, "service", "", c.flags.StringVar(&c.service, "service", "",
"Specifies the service to watch. Required for 'service' type, "+ "Specifies the service to watch. Required for 'service' type, "+
"optional for 'checks' type.") "optional for 'checks' type.")
c.flags.StringVar(&c.tag, "tag", "", c.flags.Var((*flags.AppendSliceValue)(&c.tag), "tag", "Specifies the service tag(s) to filter on. "+
"Specifies the service tag to filter on. Optional for 'service' type.") "Optional for 'service' type. May be specified multiple times")
c.flags.StringVar(&c.passingOnly, "passingonly", "", c.flags.StringVar(&c.passingOnly, "passingonly", "",
"Specifies if only hosts passing all checks are displayed. "+ "Specifies if only hosts passing all checks are displayed. "+
"Optional for 'service' type, must be one of `[true|false]`. Defaults false.") "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 != "" { if c.service != "" {
params["service"] = c.service params["service"] = c.service
} }
if c.tag != "" { if len(c.tag) > 0 {
params["tag"] = c.tag params["tag"] = c.tag
} }
if c.http.Stale() { if c.http.Stale() {

View File

@ -266,26 +266,45 @@ An example of the output of this command:
The "service" watch type is used to monitor the providers The "service" watch type is used to monitor the providers
of a single service. It requires the "service" parameter of a single service. It requires the "service" parameter
and optionally takes the parameters "tag" and "passingonly". and optionally takes the parameters "tag" and
The "tag" parameter will filter by tag, and "passingonly" is "passingonly". The "tag" parameter will filter by one or more tags.
a boolean that will filter to only the instances passing all It may be either a single string value or a slice of strings.
health checks. 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. This maps to the `/v1/health/service` API internally.
Here is an example configuration: Here is an example configuration with a single tag:
```javascript ```javascript
{ {
"type": "service", "type": "service",
"service": "redis", "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: 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: An example of the output of this command:
@ -299,7 +318,10 @@ An example of the output of this command:
"Service": { "Service": {
"ID": "redis", "ID": "redis",
"Service": "redis", "Service": "redis",
"Tags": null, "Tags": [
"bar",
"foo"
],
"Port": 8000 "Port": 8000
}, },
"Checks": [ "Checks": [