Skip to content

Commit

Permalink
Backport of NET-5530 Support response header modifiers on http-route …
Browse files Browse the repository at this point in the history
…config entry into release/1.16.x (#18725)

* NET-5530 Support response header modifiers on http-route config entry (#18646)

* Add response header filters to http-route config entry definitions

* Map response header filters from config entry when constructing route destination

* Support response header modifiers at the service level as well

* Update protobuf definitions

* Update existing unit tests

* Add response filters to route consolidation logic

* Make existing unit tests more robust

* Add missing docstring

* Add changelog entry

* Add response filter modifiers to existing integration test

* Add more robust testing for response header modifiers in the discovery chain

* Add more robust testing for request header modifiers in the discovery chain

* Modify test to verify that service filter modifiers take precedence over rule filter modifiers

* Generate deep-copy code

---------

Co-authored-by: Nathan Coleman <nathan.coleman@hashicorp.com>
  • Loading branch information
1 parent ab67ff9 commit b8a0e84
Show file tree
Hide file tree
Showing 13 changed files with 1,031 additions and 644 deletions.
3 changes: 3 additions & 0 deletions .changelog/18646.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:feature
api-gateway: Add support for response header modifiers on http-route configuration entry
```
21 changes: 12 additions & 9 deletions agent/consul/discoverychain/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@ type GatewayChainSynthesizer struct {
}

type hostnameMatch struct {
match structs.HTTPMatch
filters structs.HTTPFilters
services []structs.HTTPService
match structs.HTTPMatch
filters structs.HTTPFilters
responseFilters structs.HTTPResponseFilters
services []structs.HTTPService
}

// NewGatewayChainSynthesizer creates a new GatewayChainSynthesizer for the
Expand Down Expand Up @@ -87,9 +88,10 @@ func initHostMatches(hostname string, route *structs.HTTPRouteConfigEntry, curre
// Add all matches for this rule to the list for this hostname
for _, match := range rule.Matches {
matches = append(matches, hostnameMatch{
match: match,
filters: rule.Filters,
services: rule.Services,
match: match,
filters: rule.Filters,
responseFilters: rule.ResponseFilters,
services: rule.Services,
})
}
}
Expand Down Expand Up @@ -226,9 +228,10 @@ func consolidateHTTPRoutes(matchesByHostname map[string][]hostnameMatch, listene
// Add all rules for this hostname
for _, rule := range rules {
route.Rules = append(route.Rules, structs.HTTPRouteRule{
Matches: []structs.HTTPMatch{rule.match},
Filters: rule.filters,
Services: rule.services,
Matches: []structs.HTTPMatch{rule.match},
Filters: rule.filters,
ResponseFilters: rule.responseFilters,
Services: rule.services,
})
}

Expand Down
29 changes: 22 additions & 7 deletions agent/consul/discoverychain/gateway_httproute.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ func httpRouteToDiscoveryChain(route structs.HTTPRouteConfigEntry) (*structs.Ser
var defaults []*structs.ServiceConfigEntry

for idx, rule := range route.Rules {
modifier := httpRouteFiltersToServiceRouteHeaderModifier(rule.Filters.Headers)
requestModifier := httpRouteFiltersToServiceRouteHeaderModifier(rule.Filters.Headers)
responseModifier := httpRouteFiltersToServiceRouteHeaderModifier(rule.ResponseFilters.Headers)
prefixRewrite := httpRouteFiltersToDestinationPrefixRewrite(rule.Filters.URLRewrite)

var destination structs.ServiceRouteDestination
Expand All @@ -90,16 +91,29 @@ func httpRouteToDiscoveryChain(route structs.HTTPRouteConfigEntry) (*structs.Ser
if service.Filters.URLRewrite == nil {
servicePrefixRewrite = prefixRewrite
}
serviceModifier := httpRouteFiltersToServiceRouteHeaderModifier(service.Filters.Headers)
modifier.Add = mergeMaps(modifier.Add, serviceModifier.Add)
modifier.Set = mergeMaps(modifier.Set, serviceModifier.Set)
modifier.Remove = append(modifier.Remove, serviceModifier.Remove...)

// Merge service request header modifier(s) onto route rule modifiers
// Note: Removals for the same header may exist on the rule + the service and
// will result in idempotent duplicate values in the modifier w/ service coming last
serviceRequestModifier := httpRouteFiltersToServiceRouteHeaderModifier(service.Filters.Headers)
requestModifier.Add = mergeMaps(requestModifier.Add, serviceRequestModifier.Add)
requestModifier.Set = mergeMaps(requestModifier.Set, serviceRequestModifier.Set)
requestModifier.Remove = append(requestModifier.Remove, serviceRequestModifier.Remove...)

// Merge service response header modifier(s) onto route rule modifiers
// Note: Removals for the same header may exist on the rule + the service and
// will result in idempotent duplicate values in the modifier w/ service coming last
serviceResponseModifier := httpRouteFiltersToServiceRouteHeaderModifier(service.ResponseFilters.Headers)
responseModifier.Add = mergeMaps(responseModifier.Add, serviceResponseModifier.Add)
responseModifier.Set = mergeMaps(responseModifier.Set, serviceResponseModifier.Set)
responseModifier.Remove = append(responseModifier.Remove, serviceResponseModifier.Remove...)

destination.Service = service.Name
destination.Namespace = service.NamespaceOrDefault()
destination.Partition = service.PartitionOrDefault()
destination.PrefixRewrite = servicePrefixRewrite
destination.RequestHeaders = modifier
destination.RequestHeaders = requestModifier
destination.ResponseHeaders = responseModifier

// since we have already validated the protocol elsewhere, we
// create a new service defaults here to make sure we pass validation
Expand All @@ -115,7 +129,8 @@ func httpRouteToDiscoveryChain(route structs.HTTPRouteConfigEntry) (*structs.Ser
destination.Namespace = route.NamespaceOrDefault()
destination.Partition = route.PartitionOrDefault()
destination.PrefixRewrite = prefixRewrite
destination.RequestHeaders = modifier
destination.RequestHeaders = requestModifier
destination.ResponseHeaders = responseModifier

splitter := &structs.ServiceSplitterConfigEntry{
Kind: structs.ServiceSplitter,
Expand Down
106 changes: 104 additions & 2 deletions agent/consul/discoverychain/gateway_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -518,8 +518,70 @@ func TestGatewayChainSynthesizer_Synthesize(t *testing.T) {
Kind: structs.HTTPRoute,
Name: "http-route",
Rules: []structs.HTTPRouteRule{{
Filters: structs.HTTPFilters{
Headers: []structs.HTTPHeaderFilter{
{
Add: map[string]string{"add me to the rule request": "present"},
Set: map[string]string{"set me on the rule request": "present"},
Remove: []string{"remove me from the rule request"},
},
{
Add: map[string]string{"add me to the rule and service request": "rule"},
Set: map[string]string{"set me on the rule and service request": "rule"},
},
{
Remove: []string{"remove me from the rule and service request"},
},
},
},
ResponseFilters: structs.HTTPResponseFilters{
Headers: []structs.HTTPHeaderFilter{{
Add: map[string]string{
"add me to the rule response": "present",
"add me to the rule and service response": "rule",
},
Set: map[string]string{
"set me on the rule response": "present",
"set me on the rule and service response": "rule",
},
Remove: []string{
"remove me from the rule response",
"remove me from the rule and service response",
},
}},
},
Services: []structs.HTTPService{{
Name: "foo",
Filters: structs.HTTPFilters{
Headers: []structs.HTTPHeaderFilter{
{
Add: map[string]string{"add me to the service request": "present"},
},
{
Set: map[string]string{"set me on the service request": "present"},
Remove: []string{"remove me from the service request"},
},
{
Add: map[string]string{"add me to the rule and service request": "service"},
Set: map[string]string{"set me on the rule and service request": "service"},
Remove: []string{"remove me from the rule and service request"},
},
},
},
ResponseFilters: structs.HTTPResponseFilters{
Headers: []structs.HTTPHeaderFilter{
{
Add: map[string]string{"add me to the service response": "present"},
Set: map[string]string{"set me on the service response": "present"},
Remove: []string{"remove me from the service response"},
},
{
Add: map[string]string{"add me to the rule and service response": "service"},
Set: map[string]string{"set me on the rule and service response": "service"},
Remove: []string{"remove me from the rule and service response"},
},
},
},
}},
}},
},
Expand Down Expand Up @@ -557,8 +619,40 @@ func TestGatewayChainSynthesizer_Synthesize(t *testing.T) {
Partition: "default",
Namespace: "default",
RequestHeaders: &structs.HTTPHeaderModifiers{
Add: make(map[string]string),
Set: make(map[string]string),
Add: map[string]string{
"add me to the rule request": "present",
"add me to the service request": "present",
"add me to the rule and service request": "service",
},
Set: map[string]string{
"set me on the rule request": "present",
"set me on the service request": "present",
"set me on the rule and service request": "service",
},
Remove: []string{
"remove me from the rule request",
"remove me from the rule and service request",
"remove me from the service request",
"remove me from the rule and service request",
},
},
ResponseHeaders: &structs.HTTPHeaderModifiers{
Add: map[string]string{
"add me to the rule response": "present",
"add me to the service response": "present",
"add me to the rule and service response": "service",
},
Set: map[string]string{
"set me on the rule response": "present",
"set me on the service response": "present",
"set me on the rule and service response": "service",
},
Remove: []string{
"remove me from the rule response",
"remove me from the rule and service response",
"remove me from the service response",
"remove me from the rule and service response",
},
},
},
},
Expand Down Expand Up @@ -663,6 +757,10 @@ func TestGatewayChainSynthesizer_Synthesize(t *testing.T) {
Add: make(map[string]string),
Set: make(map[string]string),
},
ResponseHeaders: &structs.HTTPHeaderModifiers{
Add: make(map[string]string),
Set: make(map[string]string),
},
},
},
NextNode: "resolver:foo-2.default.default.dc2",
Expand Down Expand Up @@ -850,6 +948,10 @@ func TestGatewayChainSynthesizer_ComplexChain(t *testing.T) {
Add: make(map[string]string),
Set: make(map[string]string),
},
ResponseHeaders: &structs.HTTPHeaderModifiers{
Add: make(map[string]string),
Set: make(map[string]string),
},
},
},
NextNode: "splitter:splitter-one.default.default",
Expand Down
13 changes: 13 additions & 0 deletions agent/structs/config_entry_routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,12 @@ type HTTPFilters struct {
URLRewrite *URLRewrite
}

// HTTPResponseFilters specifies a list of filters used to modify the
// response returned by an upstream
type HTTPResponseFilters struct {
Headers []HTTPHeaderFilter
}

// HTTPHeaderFilter specifies how HTTP headers should be modified.
type HTTPHeaderFilter struct {
Add map[string]string
Expand All @@ -438,6 +444,9 @@ type HTTPRouteRule struct {
// Filters is a list of HTTP-based filters used to modify a request prior
// to routing it to the upstream service
Filters HTTPFilters
// ResponseFilters is a list of HTTP-based filters used to modify a response
// returned by the upstream service
ResponseFilters HTTPResponseFilters
// Matches specified the matching criteria used in the routing table. If a
// request matches the given HTTPMatch configuration, then traffic is routed
// to services specified in the Services field.
Expand All @@ -457,6 +466,10 @@ type HTTPService struct {
// to routing it to the upstream service
Filters HTTPFilters

// ResponseFilters is a list of HTTP-based filters used to modify the
// response returned from the upstream service
ResponseFilters HTTPResponseFilters

acl.EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
}

Expand Down
44 changes: 44 additions & 0 deletions agent/structs/structs.deepcopy.go
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,28 @@ func (o *HTTPRouteConfigEntry) DeepCopy() *HTTPRouteConfigEntry {
cp.Rules[i2].Filters.URLRewrite = new(URLRewrite)
*cp.Rules[i2].Filters.URLRewrite = *o.Rules[i2].Filters.URLRewrite
}
if o.Rules[i2].ResponseFilters.Headers != nil {
cp.Rules[i2].ResponseFilters.Headers = make([]HTTPHeaderFilter, len(o.Rules[i2].ResponseFilters.Headers))
copy(cp.Rules[i2].ResponseFilters.Headers, o.Rules[i2].ResponseFilters.Headers)
for i5 := range o.Rules[i2].ResponseFilters.Headers {
if o.Rules[i2].ResponseFilters.Headers[i5].Add != nil {
cp.Rules[i2].ResponseFilters.Headers[i5].Add = make(map[string]string, len(o.Rules[i2].ResponseFilters.Headers[i5].Add))
for k7, v7 := range o.Rules[i2].ResponseFilters.Headers[i5].Add {
cp.Rules[i2].ResponseFilters.Headers[i5].Add[k7] = v7
}
}
if o.Rules[i2].ResponseFilters.Headers[i5].Remove != nil {
cp.Rules[i2].ResponseFilters.Headers[i5].Remove = make([]string, len(o.Rules[i2].ResponseFilters.Headers[i5].Remove))
copy(cp.Rules[i2].ResponseFilters.Headers[i5].Remove, o.Rules[i2].ResponseFilters.Headers[i5].Remove)
}
if o.Rules[i2].ResponseFilters.Headers[i5].Set != nil {
cp.Rules[i2].ResponseFilters.Headers[i5].Set = make(map[string]string, len(o.Rules[i2].ResponseFilters.Headers[i5].Set))
for k7, v7 := range o.Rules[i2].ResponseFilters.Headers[i5].Set {
cp.Rules[i2].ResponseFilters.Headers[i5].Set[k7] = v7
}
}
}
}
if o.Rules[i2].Matches != nil {
cp.Rules[i2].Matches = make([]HTTPMatch, len(o.Rules[i2].Matches))
copy(cp.Rules[i2].Matches, o.Rules[i2].Matches)
Expand Down Expand Up @@ -427,6 +449,28 @@ func (o *HTTPRouteConfigEntry) DeepCopy() *HTTPRouteConfigEntry {
cp.Rules[i2].Services[i4].Filters.URLRewrite = new(URLRewrite)
*cp.Rules[i2].Services[i4].Filters.URLRewrite = *o.Rules[i2].Services[i4].Filters.URLRewrite
}
if o.Rules[i2].Services[i4].ResponseFilters.Headers != nil {
cp.Rules[i2].Services[i4].ResponseFilters.Headers = make([]HTTPHeaderFilter, len(o.Rules[i2].Services[i4].ResponseFilters.Headers))
copy(cp.Rules[i2].Services[i4].ResponseFilters.Headers, o.Rules[i2].Services[i4].ResponseFilters.Headers)
for i7 := range o.Rules[i2].Services[i4].ResponseFilters.Headers {
if o.Rules[i2].Services[i4].ResponseFilters.Headers[i7].Add != nil {
cp.Rules[i2].Services[i4].ResponseFilters.Headers[i7].Add = make(map[string]string, len(o.Rules[i2].Services[i4].ResponseFilters.Headers[i7].Add))
for k9, v9 := range o.Rules[i2].Services[i4].ResponseFilters.Headers[i7].Add {
cp.Rules[i2].Services[i4].ResponseFilters.Headers[i7].Add[k9] = v9
}
}
if o.Rules[i2].Services[i4].ResponseFilters.Headers[i7].Remove != nil {
cp.Rules[i2].Services[i4].ResponseFilters.Headers[i7].Remove = make([]string, len(o.Rules[i2].Services[i4].ResponseFilters.Headers[i7].Remove))
copy(cp.Rules[i2].Services[i4].ResponseFilters.Headers[i7].Remove, o.Rules[i2].Services[i4].ResponseFilters.Headers[i7].Remove)
}
if o.Rules[i2].Services[i4].ResponseFilters.Headers[i7].Set != nil {
cp.Rules[i2].Services[i4].ResponseFilters.Headers[i7].Set = make(map[string]string, len(o.Rules[i2].Services[i4].ResponseFilters.Headers[i7].Set))
for k9, v9 := range o.Rules[i2].Services[i4].ResponseFilters.Headers[i7].Set {
cp.Rules[i2].Services[i4].ResponseFilters.Headers[i7].Set[k9] = v9
}
}
}
}
}
}
}
Expand Down
14 changes: 14 additions & 0 deletions api/config_entry_routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,12 @@ type HTTPFilters struct {
URLRewrite *URLRewrite
}

// HTTPResponseFilters specifies a list of filters used to modify a
// response returned by an upstream
type HTTPResponseFilters struct {
Headers []HTTPHeaderFilter
}

// HTTPHeaderFilter specifies how HTTP headers should be modified.
type HTTPHeaderFilter struct {
Add map[string]string
Expand All @@ -216,6 +222,9 @@ type HTTPRouteRule struct {
// Filters is a list of HTTP-based filters used to modify a request prior
// to routing it to the upstream service
Filters HTTPFilters
// ResponseFilters is a list of HTTP-based filters used to modify a response
// returned by the upstream service
ResponseFilters HTTPResponseFilters
// Matches specified the matching criteria used in the routing table. If a
// request matches the given HTTPMatch configuration, then traffic is routed
// to services specified in the Services field.
Expand All @@ -231,10 +240,15 @@ type HTTPService struct {
// Weight is an arbitrary integer used in calculating how much
// traffic should be sent to the given service.
Weight int

// Filters is a list of HTTP-based filters used to modify a request prior
// to routing it to the upstream service
Filters HTTPFilters

// ResponseFilters is a list of HTTP-based filters used to modify the
// response returned from the upstream service
ResponseFilters HTTPResponseFilters

// Partition is the partition the config entry is associated with.
// Partitioning is a Consul Enterprise feature.
Partition string `json:",omitempty"`
Expand Down
Loading

0 comments on commit b8a0e84

Please sign in to comment.