diff --git a/agent/config/runtime_test.go b/agent/config/runtime_test.go index 1170eef160..8fc8b7a85f 100644 --- a/agent/config/runtime_test.go +++ b/agent/config/runtime_test.go @@ -3121,16 +3121,16 @@ func TestConfigFlagsAndEdgecases(t *testing.T) { "path_prefix": "/foo", "query_param": [ { - "name": "hack1" + "name": "hack1", + "present": true }, { "name": "hack2", - "value": "1" + "exact": "1" }, { "name": "hack3", - "value": "a.*z", - "regex": true + "regex": "a.*z" } ] } @@ -3205,15 +3205,15 @@ func TestConfigFlagsAndEdgecases(t *testing.T) { query_param = [ { name = "hack1" + present = true }, { name = "hack2" - value = "1" + exact = "1" }, { name = "hack3" - value = "a.*z" - regex = true + regex = "a.*z" }, ] } @@ -3286,16 +3286,16 @@ func TestConfigFlagsAndEdgecases(t *testing.T) { PathPrefix: "/foo", QueryParam: []structs.ServiceRouteHTTPMatchQueryParam{ { - Name: "hack1", + Name: "hack1", + Present: true, }, { Name: "hack2", - Value: "1", + Exact: "1", }, { Name: "hack3", - Value: "a.*z", - Regex: true, + Regex: "a.*z", }, }, }, diff --git a/agent/structs/config_entry_discoverychain.go b/agent/structs/config_entry_discoverychain.go index 1c929bc90a..fdfe9c527d 100644 --- a/agent/structs/config_entry_discoverychain.go +++ b/agent/structs/config_entry_discoverychain.go @@ -124,6 +124,20 @@ func (e *ServiceRouterConfigEntry) Validate() error { if qm.Name == "" { return fmt.Errorf("Route[%d] QueryParam[%d] missing required Name field", i, j) } + + qmParts := 0 + if qm.Present { + qmParts++ + } + if qm.Exact != "" { + qmParts++ + } + if qm.Regex != "" { + qmParts++ + } + if qmParts != 1 { + return fmt.Errorf("Route[%d] QueryParam[%d] should only contain one of Present, Exact, or Regex", i, j) + } } } @@ -229,9 +243,10 @@ type ServiceRouteHTTPMatchHeader struct { } type ServiceRouteHTTPMatchQueryParam struct { - Name string - Value string `json:",omitempty"` - Regex bool `json:",omitempty"` + Name string + Present bool `json:",omitempty"` + Exact string `json:",omitempty"` + Regex string `json:",omitempty"` } // ServiceRouteDestination describes how to proxy the actual matching request diff --git a/agent/structs/config_entry_discoverychain_test.go b/agent/structs/config_entry_discoverychain_test.go index 8e3310e566..d4982f91dc 100644 --- a/agent/structs/config_entry_discoverychain_test.go +++ b/agent/structs/config_entry_discoverychain_test.go @@ -960,11 +960,68 @@ func TestServiceRouterConfigEntry(t *testing.T) { { name: "route with no name query param", entry: makerouter(routeMatch(httpMatchParam(ServiceRouteHTTPMatchQueryParam{ - Value: "foo", + Exact: "foo", }))), validateErr: "missing required Name field", }, - + { + name: "route with query param exact match", + entry: makerouter(routeMatch(httpMatchParam(ServiceRouteHTTPMatchQueryParam{ + Name: "foo", + Exact: "bar", + }))), + }, + { + name: "route with query param regex match", + entry: makerouter(routeMatch(httpMatchParam(ServiceRouteHTTPMatchQueryParam{ + Name: "foo", + Regex: "bar", + }))), + }, + { + name: "route with query param present match", + entry: makerouter(routeMatch(httpMatchParam(ServiceRouteHTTPMatchQueryParam{ + Name: "foo", + Present: true, + }))), + }, + { + name: "route with query param exact and regex match", + entry: makerouter(routeMatch(httpMatchParam(ServiceRouteHTTPMatchQueryParam{ + Name: "foo", + Exact: "bar", + Regex: "bar", + }))), + validateErr: "should only contain one of Present, Exact, or Regex", + }, + { + name: "route with query param exact and present match", + entry: makerouter(routeMatch(httpMatchParam(ServiceRouteHTTPMatchQueryParam{ + Name: "foo", + Exact: "bar", + Present: true, + }))), + validateErr: "should only contain one of Present, Exact, or Regex", + }, + { + name: "route with query param regex and present match", + entry: makerouter(routeMatch(httpMatchParam(ServiceRouteHTTPMatchQueryParam{ + Name: "foo", + Regex: "bar", + Present: true, + }))), + validateErr: "should only contain one of Present, Exact, or Regex", + }, + { + name: "route with query param exact, regex, and present match", + entry: makerouter(routeMatch(httpMatchParam(ServiceRouteHTTPMatchQueryParam{ + Name: "foo", + Exact: "bar", + Regex: "bar", + Present: true, + }))), + validateErr: "should only contain one of Present, Exact, or Regex", + }, //////////////// { name: "route with no match and prefix rewrite", @@ -1033,7 +1090,7 @@ func TestServiceRouterConfigEntry(t *testing.T) { entry: makerouter(ServiceRoute{ Match: httpMatchParam(ServiceRouteHTTPMatchQueryParam{ Name: "foo", - Value: "bar", + Exact: "bar", }), Destination: &ServiceRouteDestination{ Service: "other", diff --git a/agent/structs/config_entry_test.go b/agent/structs/config_entry_test.go index e4cfc591e9..0e8f7fce95 100644 --- a/agent/structs/config_entry_test.go +++ b/agent/structs/config_entry_test.go @@ -171,15 +171,15 @@ func TestDecodeConfigEntry(t *testing.T) { query_param = [ { name = "hack1" + present = true }, { name = "hack2" - value = "1" + exact = "1" }, { name = "hack3" - value = "a.*z" - regex = true + regex = "a.*z" }, ] } @@ -249,15 +249,15 @@ func TestDecodeConfigEntry(t *testing.T) { QueryParam = [ { Name = "hack1" + Present = true }, { Name = "hack2" - Value = "1" + Exact = "1" }, { Name = "hack3" - Value = "a.*z" - Regex = true + Regex = "a.*z" }, ] } @@ -326,16 +326,16 @@ func TestDecodeConfigEntry(t *testing.T) { PathPrefix: "/foo", QueryParam: []ServiceRouteHTTPMatchQueryParam{ { - Name: "hack1", + Name: "hack1", + Present: true, }, { Name: "hack2", - Value: "1", + Exact: "1", }, { Name: "hack3", - Value: "a.*z", - Regex: true, + Regex: "a.*z", }, }, }, diff --git a/agent/xds/routes.go b/agent/xds/routes.go index d8569db862..b5f566c99f 100644 --- a/agent/xds/routes.go +++ b/agent/xds/routes.go @@ -256,9 +256,19 @@ func makeRouteMatchForDiscoveryRoute(discoveryRoute *structs.DiscoveryRoute, pro em.QueryParameters = make([]*envoyroute.QueryParameterMatcher, 0, len(match.HTTP.QueryParam)) for _, qm := range match.HTTP.QueryParam { eq := &envoyroute.QueryParameterMatcher{ - Name: qm.Name, - Value: qm.Value, - Regex: makeBoolValue(qm.Regex), + Name: qm.Name, + } + + switch { + case qm.Exact != "": + eq.Value = qm.Exact + case qm.Regex != "": + eq.Value = qm.Regex + eq.Regex = makeBoolValue(true) + case qm.Present: + eq.Value = "" + default: + continue // skip this impossible situation } em.QueryParameters = append(em.QueryParameters, eq) diff --git a/agent/xds/routes_test.go b/agent/xds/routes_test.go index 23c241ce3f..2c102e72f2 100644 --- a/agent/xds/routes_test.go +++ b/agent/xds/routes_test.go @@ -192,19 +192,25 @@ func TestRoutesFromSnapshot(t *testing.T) { }, { Match: httpMatchParam(structs.ServiceRouteHTTPMatchQueryParam{ - Name: "secretparam", - Value: "exact", + Name: "secretparam1", + Exact: "exact", }), Destination: toService("prm-exact"), }, { Match: httpMatchParam(structs.ServiceRouteHTTPMatchQueryParam{ - Name: "secretparam", - Value: "regex", - Regex: true, + Name: "secretparam2", + Regex: "regex", }), Destination: toService("prm-regex"), }, + { + Match: httpMatchParam(structs.ServiceRouteHTTPMatchQueryParam{ + Name: "secretparam3", + Present: true, + }), + Destination: toService("prm-present"), + }, { Match: nil, Destination: toService("nil-match"), diff --git a/agent/xds/testdata/routes/connect-proxy-with-chain-and-router.golden b/agent/xds/testdata/routes/connect-proxy-with-chain-and-router.golden index 5d2f56c6f9..c61146ac5b 100644 --- a/agent/xds/testdata/routes/connect-proxy-with-chain-and-router.golden +++ b/agent/xds/testdata/routes/connect-proxy-with-chain-and-router.golden @@ -125,9 +125,8 @@ "prefix": "/", "queryParameters": [ { - "name": "secretparam", - "value": "exact", - "regex": false + "name": "secretparam1", + "value": "exact" } ] }, @@ -140,7 +139,7 @@ "prefix": "/", "queryParameters": [ { - "name": "secretparam", + "name": "secretparam2", "value": "regex", "regex": true } @@ -150,6 +149,19 @@ "cluster": "prm-regex.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul" } }, + { + "match": { + "prefix": "/", + "queryParameters": [ + { + "name": "secretparam3" + } + ] + }, + "route": { + "cluster": "prm-present.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul" + } + }, { "match": { "prefix": "/" diff --git a/api/config_entry_discoverychain.go b/api/config_entry_discoverychain.go index 702b38b613..61c0bbb32a 100644 --- a/api/config_entry_discoverychain.go +++ b/api/config_entry_discoverychain.go @@ -49,9 +49,10 @@ type ServiceRouteHTTPMatchHeader struct { } type ServiceRouteHTTPMatchQueryParam struct { - Name string - Value string `json:",omitempty"` - Regex bool `json:",omitempty"` + Name string + Present bool `json:",omitempty"` + Exact string `json:",omitempty"` + Regex string `json:",omitempty"` } type ServiceRouteDestination struct { diff --git a/api/config_entry_discoverychain_test.go b/api/config_entry_discoverychain_test.go index 9667402eec..a761d03678 100644 --- a/api/config_entry_discoverychain_test.go +++ b/api/config_entry_discoverychain_test.go @@ -200,7 +200,7 @@ func TestAPI_ConfigEntry_DiscoveryChain(t *testing.T) { {Name: "x-debug", Exact: "1"}, }, QueryParam: []ServiceRouteHTTPMatchQueryParam{ - {Name: "debug", Value: "1"}, + {Name: "debug", Exact: "1"}, }, }, }, diff --git a/command/config/write/config_write_test.go b/command/config/write/config_write_test.go index 8e77bb9bc3..45f6f564f0 100644 --- a/command/config/write/config_write_test.go +++ b/command/config/write/config_write_test.go @@ -265,15 +265,15 @@ func TestParseConfigEntry(t *testing.T) { query_param = [ { name = "hack1" + present = true }, { name = "hack2" - value = "1" + exact = "1" }, { name = "hack3" - value = "a.*z" - regex = true + regex = "a.*z" }, ] } @@ -343,15 +343,15 @@ func TestParseConfigEntry(t *testing.T) { QueryParam = [ { Name = "hack1" + Present = true }, { Name = "hack2" - Value = "1" + Exact = "1" }, { Name = "hack3" - Value = "a.*z" - Regex = true + Regex = "a.*z" }, ] } @@ -420,16 +420,16 @@ func TestParseConfigEntry(t *testing.T) { PathPrefix: "/foo", QueryParam: []api.ServiceRouteHTTPMatchQueryParam{ { - Name: "hack1", + Name: "hack1", + Present: true, }, { Name: "hack2", - Value: "1", + Exact: "1", }, { Name: "hack3", - Value: "a.*z", - Regex: true, + Regex: "a.*z", }, }, }, diff --git a/website/source/docs/agent/config-entries/service-router.html.md b/website/source/docs/agent/config-entries/service-router.html.md index 59860b6597..3184deb936 100644 --- a/website/source/docs/agent/config-entries/service-router.html.md +++ b/website/source/docs/agent/config-entries/service-router.html.md @@ -84,7 +84,7 @@ routes = [ query_param = [ { name = "x-debug" - value = "1" + exact = "1" }, ] } @@ -176,16 +176,22 @@ routes = [ - `Name` `(string: )` - The name of the query parameter to match on. - - `Value` `(string: )` - String to match against the query - parameter value. The behavior changes with the definition of the - `Regex` field. - - - `Regex` `(bool: false)` - Controls how the `Value` field is used. If - `Regex` is `false` then `Value` matches exactly. If `Regex` is - `true` then `Value` matches as a regular expression pattern. + - `Present` `(bool: false)` - Match if the query parameter with the given name + is present with any value. - The syntax when using the Envoy proxy is [documented - here](https://en.cppreference.com/w/cpp/regex/ecmascript). + At most only one of `Exact`, `Regex`, or `Present` may be configured. + + - `Exact` `(string: "")` - Match if the query parameter with the given + name is this value. + + At most only one of `Exact`, `Regex`, or `Present` may be configured. + + - `Regex` `(string: "")` - Match if the query parameter with the given + name matches this pattern. + + The syntax when using the Envoy proxy is [documented here](https://en.cppreference.com/w/cpp/regex/ecmascript). + + At most only one of `Exact`, `Regex`, or `Present` may be configured. - `Destination` `(ServiceRouteDestination: )` - Controls how to proxy the actual matching request to a service.