Merge pull request #8290 from hashicorp/dnephin/watch-decode

watch: fix script watches with single arg
This commit is contained in:
Daniel Nephin 2020-07-20 14:41:17 -04:00 committed by hashicorp-ci
parent e5cfe66b0c
commit 65566e2c98
3 changed files with 170 additions and 38 deletions

View File

@ -1332,44 +1332,10 @@ func (a *Agent) reloadWatches(cfg *config.RuntimeConfig) error {
} }
} }
// Parse the watches, excluding 'handler' and 'args' wp, err := makeWatchPlan(a.logger, params)
wp, err := watch.ParseExempt(params, []string{"handler", "args"})
if err != nil { if err != nil {
return fmt.Errorf("Failed to parse watch (%#v): %v", params, err) return err
} }
// Get the handler and subprocess arguments
handler, hasHandler := wp.Exempt["handler"]
args, hasArgs := wp.Exempt["args"]
if hasHandler {
a.logger.Warn("The 'handler' field in watches has been deprecated " +
"and replaced with the 'args' field. See https://www.consul.io/docs/agent/watches.html")
}
if _, ok := handler.(string); hasHandler && !ok {
return fmt.Errorf("Watch handler must be a string")
}
if raw, ok := args.([]interface{}); hasArgs && ok {
var parsed []string
for _, arg := range raw {
v, ok := arg.(string)
if !ok {
return fmt.Errorf("Watch args must be a list of strings")
}
parsed = append(parsed, v)
}
wp.Exempt["args"] = parsed
} else if hasArgs && !ok {
return fmt.Errorf("Watch args must be a list of strings")
}
if hasHandler && hasArgs || hasHandler && wp.HandlerType == "http" || hasArgs && wp.HandlerType == "http" {
return fmt.Errorf("Only one watch handler allowed")
}
if !hasHandler && !hasArgs && wp.HandlerType != "http" {
return fmt.Errorf("Must define a watch handler")
}
// Store the watch plan
watchPlans = append(watchPlans, wp) watchPlans = append(watchPlans, wp)
} }

View File

@ -184,3 +184,59 @@ func makeHTTPWatchHandler(logger hclog.Logger, config *watch.HttpHandlerConfig)
} }
return fn return fn
} }
// TODO: return a fully constructed watch.Plan with a Plan.Handler, so that Exempt
// can be ignored by the caller.
func makeWatchPlan(logger hclog.Logger, params map[string]interface{}) (*watch.Plan, error) {
wp, err := watch.ParseExempt(params, []string{"handler", "args"})
if err != nil {
return nil, fmt.Errorf("Failed to parse watch (%#v): %v", params, err)
}
handler, hasHandler := wp.Exempt["handler"]
if hasHandler {
logger.Warn("The 'handler' field in watches has been deprecated " +
"and replaced with the 'args' field. See https://www.consul.io/docs/agent/watches.html")
}
if _, ok := handler.(string); hasHandler && !ok {
return nil, fmt.Errorf("Watch handler must be a string")
}
args, hasArgs := wp.Exempt["args"]
if hasArgs {
wp.Exempt["args"], err = parseWatchArgs(args)
if err != nil {
return nil, err
}
}
if hasHandler && hasArgs || hasHandler && wp.HandlerType == "http" || hasArgs && wp.HandlerType == "http" {
return nil, fmt.Errorf("Only one watch handler allowed")
}
if !hasHandler && !hasArgs && wp.HandlerType != "http" {
return nil, fmt.Errorf("Must define a watch handler")
}
return wp, nil
}
func parseWatchArgs(args interface{}) ([]string, error) {
switch args := args.(type) {
case string:
return []string{args}, nil
case []string:
return args, nil
case []interface{}:
result := make([]string, 0, len(args))
for _, arg := range args {
v, ok := arg.(string)
if !ok {
return nil, fmt.Errorf("Watch args must be a list of strings")
}
result = append(result, v)
}
return result, nil
default:
return nil, fmt.Errorf("Watch args must be a list of strings")
}
}

View File

@ -10,10 +10,11 @@ import (
"github.com/hashicorp/consul/api/watch" "github.com/hashicorp/consul/api/watch"
"github.com/hashicorp/consul/sdk/testutil" "github.com/hashicorp/consul/sdk/testutil"
"github.com/hashicorp/go-hclog"
"github.com/stretchr/testify/require"
) )
func TestMakeWatchHandler(t *testing.T) { func TestMakeWatchHandler(t *testing.T) {
t.Parallel()
defer os.Remove("handler_out") defer os.Remove("handler_out")
defer os.Remove("handler_index_out") defer os.Remove("handler_index_out")
script := "bash -c 'echo $CONSUL_INDEX >> handler_index_out && cat >> handler_out'" script := "bash -c 'echo $CONSUL_INDEX >> handler_index_out && cat >> handler_out'"
@ -36,7 +37,6 @@ func TestMakeWatchHandler(t *testing.T) {
} }
func TestMakeHTTPWatchHandler(t *testing.T) { func TestMakeHTTPWatchHandler(t *testing.T) {
t.Parallel()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
idx := r.Header.Get("X-Consul-Index") idx := r.Header.Get("X-Consul-Index")
if idx != "100" { if idx != "100" {
@ -65,3 +65,113 @@ func TestMakeHTTPWatchHandler(t *testing.T) {
handler := makeHTTPWatchHandler(testutil.Logger(t), &config) handler := makeHTTPWatchHandler(testutil.Logger(t), &config)
handler(100, []string{"foo", "bar", "baz"}) handler(100, []string{"foo", "bar", "baz"})
} }
type raw map[string]interface{}
func TestMakeWatchPlan(t *testing.T) {
type testCase struct {
name string
params map[string]interface{}
expected func(t *testing.T, plan *watch.Plan)
expectedErr string
}
fn := func(t *testing.T, tc testCase) {
plan, err := makeWatchPlan(hclog.New(nil), tc.params)
if tc.expectedErr != "" {
require.Error(t, err)
require.Contains(t, err.Error(), tc.expectedErr)
return
}
require.NoError(t, err)
tc.expected(t, plan)
}
var testCases = []testCase{
{
name: "handler_type script, with deprecated handler field",
params: raw{
"type": "key",
"key": "foo",
"handler_type": "script",
"handler": "./script.sh",
},
expected: func(t *testing.T, plan *watch.Plan) {
require.Equal(t, plan.HandlerType, "script")
require.Equal(t, plan.Exempt["handler"], "./script.sh")
},
},
{
name: "handler_type script, with single arg",
params: raw{
"type": "key",
"key": "foo",
"handler_type": "script",
"args": "./script.sh",
},
expected: func(t *testing.T, plan *watch.Plan) {
require.Equal(t, plan.HandlerType, "script")
require.Equal(t, plan.Exempt["args"], []string{"./script.sh"})
},
},
{
name: "handler_type script, with multiple args from slice of interface",
params: raw{
"type": "key",
"key": "foo",
"handler_type": "script",
"args": []interface{}{"./script.sh", "arg1"},
},
expected: func(t *testing.T, plan *watch.Plan) {
require.Equal(t, plan.HandlerType, "script")
require.Equal(t, plan.Exempt["args"], []string{"./script.sh", "arg1"})
},
},
{
name: "handler_type script, with multiple args from slice of strings",
params: raw{
"type": "key",
"key": "foo",
"handler_type": "script",
"args": []string{"./script.sh", "arg1"},
},
expected: func(t *testing.T, plan *watch.Plan) {
require.Equal(t, plan.HandlerType, "script")
require.Equal(t, plan.Exempt["args"], []string{"./script.sh", "arg1"})
},
},
{
name: "handler_type script, with not string args",
params: raw{
"type": "key",
"key": "foo",
"handler_type": "script",
"args": []interface{}{"./script.sh", true},
},
expectedErr: "Watch args must be a list of strings",
},
{
name: "conflicting handler",
params: raw{
"type": "key",
"key": "foo",
"handler_type": "script",
"handler": "./script.sh",
"args": []interface{}{"arg1"},
},
expectedErr: "Only one watch handler allowed",
},
{
name: "no handler_type",
params: raw{
"type": "key",
"key": "foo",
},
expectedErr: "Must define a watch handler",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
fn(t, tc)
})
}
}