Ingress gateway header manip plumbing

This commit is contained in:
Paul Banks 2021-07-13 13:53:59 +01:00
parent d776a2d236
commit f439dfc04f
11 changed files with 955 additions and 4 deletions

View File

@ -54,6 +54,7 @@ func (s *handlerIngressGateway) initialize(ctx context.Context) (ConfigSnapshot,
snap.IngressGateway.WatchedUpstreamEndpoints = make(map[string]map[string]structs.CheckServiceNodes)
snap.IngressGateway.WatchedGateways = make(map[string]map[string]context.CancelFunc)
snap.IngressGateway.WatchedGatewayEndpoints = make(map[string]map[string]structs.CheckServiceNodes)
snap.IngressGateway.Listeners = make(map[IngressListenerKey]structs.IngressListener)
return snap, nil
}
@ -82,6 +83,13 @@ func (s *handlerIngressGateway) handleUpdate(ctx context.Context, u cache.Update
snap.IngressGateway.TLSEnabled = gatewayConf.TLS.Enabled
snap.IngressGateway.TLSSet = true
// Load each listener's config from the config entry so we don't have to
// pass listener config through "upstreams" types as that grows.
for _, l := range gatewayConf.Listeners {
key := IngressListenerKey{Protocol: l.Protocol, Port: l.Port}
snap.IngressGateway.Listeners[key] = l
}
if err := s.watchIngressLeafCert(ctx, snap); err != nil {
return err
}

View File

@ -321,6 +321,10 @@ type configSnapshotIngressGateway struct {
// to. This is constructed from the ingress-gateway config entry, and uses
// the GatewayServices RPC to retrieve them.
Upstreams map[IngressListenerKey]structs.Upstreams
// Listeners is the original listener config from the ingress-gateway config
// entry to save us trying to pass fields through Upstreams
Listeners map[IngressListenerKey]structs.IngressListener
}
func (c *configSnapshotIngressGateway) IsEmpty() bool {

View File

@ -1681,6 +1681,15 @@ func testConfigSnapshotIngressGateway(
},
},
},
Listeners: map[IngressListenerKey]structs.IngressListener{
{protocol, 9191}: {
Port: 9191,
Protocol: protocol,
Services: []structs.IngressService{
{Name: "db"},
},
},
},
}
}
return snap

View File

@ -168,6 +168,7 @@ func (e *IngressGatewayConfigEntry) Validate() error {
}
declaredHosts := make(map[string]bool)
serviceNames := make(map[ServiceID]struct{})
for i, s := range listener.Services {
if err := validateInnerEnterpriseMeta(&s.EnterpriseMeta, &e.EnterpriseMeta); err != nil {
return fmt.Errorf("Services[%d].%v", i, err)
@ -197,6 +198,11 @@ func (e *IngressGatewayConfigEntry) Validate() error {
if s.NamespaceOrDefault() == WildcardSpecifier {
return fmt.Errorf("Wildcard namespace is not supported for ingress services (listener on port %d)", listener.Port)
}
sid := NewServiceID(s.Name, &s.EnterpriseMeta)
if _, ok := serviceNames[sid]; ok {
return fmt.Errorf("Service %s cannot be added multiple times (listener on port %d)", sid, listener.Port)
}
serviceNames[sid] = struct{}{}
for _, h := range s.Hosts {
if declaredHosts[h] {

View File

@ -523,6 +523,27 @@ func TestIngressGatewayConfigEntry(t *testing.T) {
},
validateErr: "response headers only valid for http",
},
"duplicate services not allowed": {
entry: &IngressGatewayConfigEntry{
Kind: "ingress-gateway",
Name: "ingress-web",
Listeners: []IngressListener{
{
Port: 1111,
Protocol: "http",
Services: []IngressService{
{
Name: "web",
},
{
Name: "web",
},
},
},
},
},
validateErr: "Service web cannot be added multiple times (listener on port 1111)",
},
}
testConfigEntryNormalizeAndValidate(t, cases)

View File

@ -375,9 +375,11 @@ type Upstream struct {
// MeshGateway is the configuration for mesh gateway usage of this upstream
MeshGateway MeshGatewayConfig `json:",omitempty" alias:"mesh_gateway"`
// IngressHosts are a list of hosts that should route to this upstream from
// an ingress gateway. This cannot and should not be set by a user, it is
// used internally to store the association of hosts to an upstream service.
// IngressHosts are a list of hosts that should route to this upstream from an
// ingress gateway. This cannot and should not be set by a user, it is used
// internally to store the association of hosts to an upstream service.
// TODO(banks): we shouldn't need this any more now we pass through full
// listener config in the ingress snapshot.
IngressHosts []string `json:"-" bexpr:"-"`
// CentrallyConfigured indicates whether the upstream was defined in a proxy

View File

@ -7,6 +7,7 @@ import (
"strings"
"time"
envoy_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
envoy_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
envoy_matcher_v3 "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
@ -29,7 +30,11 @@ func (s *ResourceGenerator) routesFromSnapshot(cfgSnap *proxycfg.ConfigSnapshot)
case structs.ServiceKindConnectProxy:
return s.routesForConnectProxy(cfgSnap.ConnectProxy.DiscoveryChain)
case structs.ServiceKindIngressGateway:
return s.routesForIngressGateway(cfgSnap.IngressGateway.Upstreams, cfgSnap.IngressGateway.DiscoveryChain)
return s.routesForIngressGateway(
cfgSnap.IngressGateway.Listeners,
cfgSnap.IngressGateway.Upstreams,
cfgSnap.IngressGateway.DiscoveryChain,
)
case structs.ServiceKindTerminatingGateway:
return s.routesFromSnapshotTerminatingGateway(cfgSnap)
case structs.ServiceKindMeshGateway:
@ -160,6 +165,7 @@ func makeNamedDefaultRouteWithLB(clusterName string, lb *structs.LoadBalancer, a
// routesForIngressGateway returns the xDS API representation of the
// "routes" in the snapshot.
func (s *ResourceGenerator) routesForIngressGateway(
listeners map[proxycfg.IngressListenerKey]structs.IngressListener,
upstreams map[proxycfg.IngressListenerKey]structs.Upstreams,
chains map[string]*structs.CompiledDiscoveryChain,
) ([]proto.Message, error) {
@ -190,6 +196,42 @@ func (s *ResourceGenerator) routesForIngressGateway(
if err != nil {
return nil, err
}
// See if we need to configure any special settings on this route config
if lCfg, ok := listeners[listenerKey]; ok {
if is := findIngressServiceMatchingUpstream(lCfg, u); is != nil {
// Set up any header manipulation we need
if is.RequestHeaders != nil {
virtualHost.RequestHeadersToAdd = append(
virtualHost.RequestHeadersToAdd,
makeHeadersValueOptions(is.RequestHeaders.Add, true)...,
)
virtualHost.RequestHeadersToAdd = append(
virtualHost.RequestHeadersToAdd,
makeHeadersValueOptions(is.RequestHeaders.Set, false)...,
)
virtualHost.RequestHeadersToRemove = append(
virtualHost.RequestHeadersToRemove,
is.RequestHeaders.Remove...,
)
}
if is.ResponseHeaders != nil {
virtualHost.ResponseHeadersToAdd = append(
virtualHost.ResponseHeadersToAdd,
makeHeadersValueOptions(is.ResponseHeaders.Add, true)...,
)
virtualHost.ResponseHeadersToAdd = append(
virtualHost.ResponseHeadersToAdd,
makeHeadersValueOptions(is.ResponseHeaders.Set, false)...,
)
virtualHost.ResponseHeadersToRemove = append(
virtualHost.ResponseHeadersToRemove,
is.ResponseHeaders.Remove...,
)
}
}
}
upstreamRoute.VirtualHosts = append(upstreamRoute.VirtualHosts, virtualHost)
}
@ -199,6 +241,36 @@ func (s *ResourceGenerator) routesForIngressGateway(
return result, nil
}
func makeHeadersValueOptions(vals map[string]string, add bool) []*envoy_core_v3.HeaderValueOption {
opts := make([]*envoy_core_v3.HeaderValueOption, 0, len(vals))
for k, v := range vals {
o := &envoy_core_v3.HeaderValueOption{
Header: &envoy_core_v3.HeaderValue{
Key: k,
Value: v,
},
Append: makeBoolValue(add),
}
opts = append(opts, o)
}
return opts
}
func findIngressServiceMatchingUpstream(l structs.IngressListener, u structs.Upstream) *structs.IngressService {
// Hunt through for the matching service. We validate now that there is
// only one IngressService for each unique name although originally that
// wasn't checked as it didn't matter. Assume there is only one now
// though!
wantSID := u.DestinationID()
for _, s := range l.Services {
sid := structs.NewServiceID(s.Name, &s.EnterpriseMeta)
if wantSID.Matches(sid) {
return &s
}
}
return nil
}
func generateUpstreamIngressDomains(listenerKey proxycfg.IngressListenerKey, u structs.Upstream) []string {
var domains []string
domainsSet := make(map[string]bool)

View File

@ -189,6 +189,33 @@ func TestRoutesFromSnapshot(t *testing.T) {
}
},
},
{
name: "ingress-with-chain-and-router-header-manip",
create: proxycfg.TestConfigSnapshotIngressWithRouter,
setup: func(snap *proxycfg.ConfigSnapshot) {
k := proxycfg.IngressListenerKey{Port: 9191, Protocol: "http"}
l := snap.IngressGateway.Listeners[k]
l.Services[0].RequestHeaders = &structs.HTTPHeaderModifiers{
Add: map[string]string{
"foo": "bar",
},
Set: map[string]string{
"bar": "baz",
},
Remove: []string{"qux"},
}
l.Services[0].ResponseHeaders = &structs.HTTPHeaderModifiers{
Add: map[string]string{
"foo": "bar",
},
Set: map[string]string{
"bar": "baz",
},
Remove: []string{"qux"},
}
snap.IngressGateway.Listeners[k] = l
},
},
{
name: "terminating-gateway-lb-config",
create: proxycfg.TestConfigSnapshotTerminatingGateway,

View File

@ -0,0 +1,401 @@
{
"versionInfo": "00000001",
"resources": [
{
"@type": "type.googleapis.com/envoy.config.route.v3.RouteConfiguration",
"name": "9191",
"virtualHosts": [
{
"name": "db",
"domains": [
"db.ingress.*",
"db.ingress.*:9191"
],
"routes": [
{
"match": {
"prefix": "/prefix"
},
"route": {
"cluster": "prefix.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
},
{
"match": {
"path": "/exact"
},
"route": {
"cluster": "exact.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
},
{
"match": {
"safeRegex": {
"googleRe2": {
},
"regex": "/regex"
}
},
"route": {
"cluster": "regex.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
},
{
"match": {
"prefix": "/",
"headers": [
{
"name": "x-debug",
"presentMatch": true
}
]
},
"route": {
"cluster": "hdr-present.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
},
{
"match": {
"prefix": "/",
"headers": [
{
"name": "x-debug",
"presentMatch": true,
"invertMatch": true
}
]
},
"route": {
"cluster": "hdr-not-present.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
},
{
"match": {
"prefix": "/",
"headers": [
{
"name": "x-debug",
"exactMatch": "exact"
}
]
},
"route": {
"cluster": "hdr-exact.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
},
{
"match": {
"prefix": "/",
"headers": [
{
"name": "x-debug",
"prefixMatch": "prefix"
}
]
},
"route": {
"cluster": "hdr-prefix.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
},
{
"match": {
"prefix": "/",
"headers": [
{
"name": "x-debug",
"suffixMatch": "suffix"
}
]
},
"route": {
"cluster": "hdr-suffix.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
},
{
"match": {
"prefix": "/",
"headers": [
{
"name": "x-debug",
"safeRegexMatch": {
"googleRe2": {
},
"regex": "regex"
}
}
]
},
"route": {
"cluster": "hdr-regex.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
},
{
"match": {
"prefix": "/",
"headers": [
{
"name": ":method",
"safeRegexMatch": {
"googleRe2": {
},
"regex": "GET|PUT"
}
}
]
},
"route": {
"cluster": "just-methods.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
},
{
"match": {
"prefix": "/",
"headers": [
{
"name": "x-debug",
"exactMatch": "exact"
},
{
"name": ":method",
"safeRegexMatch": {
"googleRe2": {
},
"regex": "GET|PUT"
}
}
]
},
"route": {
"cluster": "hdr-exact-with-method.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
},
{
"match": {
"prefix": "/",
"queryParameters": [
{
"name": "secretparam1",
"stringMatch": {
"exact": "exact"
}
}
]
},
"route": {
"cluster": "prm-exact.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
},
{
"match": {
"prefix": "/",
"queryParameters": [
{
"name": "secretparam2",
"stringMatch": {
"safeRegex": {
"googleRe2": {
},
"regex": "regex"
}
}
}
]
},
"route": {
"cluster": "prm-regex.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
},
{
"match": {
"prefix": "/",
"queryParameters": [
{
"name": "secretparam3",
"presentMatch": true
}
]
},
"route": {
"cluster": "prm-present.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
},
{
"match": {
"prefix": "/"
},
"route": {
"cluster": "nil-match.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
},
{
"match": {
"prefix": "/"
},
"route": {
"cluster": "empty-match-1.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
},
{
"match": {
"prefix": "/"
},
"route": {
"cluster": "empty-match-2.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
},
{
"match": {
"prefix": "/prefix"
},
"route": {
"cluster": "prefix-rewrite-1.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
"prefixRewrite": "/"
}
},
{
"match": {
"prefix": "/prefix"
},
"route": {
"cluster": "prefix-rewrite-2.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
"prefixRewrite": "/nested/newlocation"
}
},
{
"match": {
"prefix": "/timeout"
},
"route": {
"cluster": "req-timeout.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
"timeout": "33s"
}
},
{
"match": {
"prefix": "/retry-connect"
},
"route": {
"cluster": "retry-connect.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
"retryPolicy": {
"retryOn": "connect-failure",
"numRetries": 15
}
}
},
{
"match": {
"prefix": "/retry-codes"
},
"route": {
"cluster": "retry-codes.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
"retryPolicy": {
"retryOn": "retriable-status-codes",
"numRetries": 15,
"retriableStatusCodes": [
401,
409,
451
]
}
}
},
{
"match": {
"prefix": "/retry-both"
},
"route": {
"cluster": "retry-both.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
"retryPolicy": {
"retryOn": "connect-failure,retriable-status-codes",
"retriableStatusCodes": [
401,
409,
451
]
}
}
},
{
"match": {
"prefix": "/split-3-ways"
},
"route": {
"weightedClusters": {
"clusters": [
{
"name": "big-side.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
"weight": 9550
},
{
"name": "goldilocks-side.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
"weight": 400
},
{
"name": "lil-bit-side.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
"weight": 50
}
],
"totalWeight": 10000
}
}
},
{
"match": {
"prefix": "/"
},
"route": {
"cluster": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
}
],
"requestHeadersToAdd": [
{
"header": {
"key": "foo",
"value": "bar"
},
"append": true
},
{
"header": {
"key": "bar",
"value": "baz"
},
"append": false
}
],
"requestHeadersToRemove": [
"qux"
],
"responseHeadersToAdd": [
{
"header": {
"key": "foo",
"value": "bar"
},
"append": true
},
{
"header": {
"key": "bar",
"value": "baz"
},
"append": false
}
],
"responseHeadersToRemove": [
"qux"
]
}
],
"validateClusters": true
}
],
"typeUrl": "type.googleapis.com/envoy.config.route.v3.RouteConfiguration",
"nonce": "00000001"
}

View File

@ -0,0 +1,401 @@
{
"versionInfo": "00000001",
"resources": [
{
"@type": "type.googleapis.com/envoy.api.v2.RouteConfiguration",
"name": "9191",
"virtualHosts": [
{
"name": "db",
"domains": [
"db.ingress.*",
"db.ingress.*:9191"
],
"routes": [
{
"match": {
"prefix": "/prefix"
},
"route": {
"cluster": "prefix.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
},
{
"match": {
"path": "/exact"
},
"route": {
"cluster": "exact.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
},
{
"match": {
"safeRegex": {
"googleRe2": {
},
"regex": "/regex"
}
},
"route": {
"cluster": "regex.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
},
{
"match": {
"prefix": "/",
"headers": [
{
"name": "x-debug",
"presentMatch": true
}
]
},
"route": {
"cluster": "hdr-present.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
},
{
"match": {
"prefix": "/",
"headers": [
{
"name": "x-debug",
"presentMatch": true,
"invertMatch": true
}
]
},
"route": {
"cluster": "hdr-not-present.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
},
{
"match": {
"prefix": "/",
"headers": [
{
"name": "x-debug",
"exactMatch": "exact"
}
]
},
"route": {
"cluster": "hdr-exact.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
},
{
"match": {
"prefix": "/",
"headers": [
{
"name": "x-debug",
"prefixMatch": "prefix"
}
]
},
"route": {
"cluster": "hdr-prefix.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
},
{
"match": {
"prefix": "/",
"headers": [
{
"name": "x-debug",
"suffixMatch": "suffix"
}
]
},
"route": {
"cluster": "hdr-suffix.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
},
{
"match": {
"prefix": "/",
"headers": [
{
"name": "x-debug",
"safeRegexMatch": {
"googleRe2": {
},
"regex": "regex"
}
}
]
},
"route": {
"cluster": "hdr-regex.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
},
{
"match": {
"prefix": "/",
"headers": [
{
"name": ":method",
"safeRegexMatch": {
"googleRe2": {
},
"regex": "GET|PUT"
}
}
]
},
"route": {
"cluster": "just-methods.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
},
{
"match": {
"prefix": "/",
"headers": [
{
"name": "x-debug",
"exactMatch": "exact"
},
{
"name": ":method",
"safeRegexMatch": {
"googleRe2": {
},
"regex": "GET|PUT"
}
}
]
},
"route": {
"cluster": "hdr-exact-with-method.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
},
{
"match": {
"prefix": "/",
"queryParameters": [
{
"name": "secretparam1",
"stringMatch": {
"exact": "exact"
}
}
]
},
"route": {
"cluster": "prm-exact.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
},
{
"match": {
"prefix": "/",
"queryParameters": [
{
"name": "secretparam2",
"stringMatch": {
"safeRegex": {
"googleRe2": {
},
"regex": "regex"
}
}
}
]
},
"route": {
"cluster": "prm-regex.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
},
{
"match": {
"prefix": "/",
"queryParameters": [
{
"name": "secretparam3",
"presentMatch": true
}
]
},
"route": {
"cluster": "prm-present.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
},
{
"match": {
"prefix": "/"
},
"route": {
"cluster": "nil-match.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
},
{
"match": {
"prefix": "/"
},
"route": {
"cluster": "empty-match-1.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
},
{
"match": {
"prefix": "/"
},
"route": {
"cluster": "empty-match-2.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
},
{
"match": {
"prefix": "/prefix"
},
"route": {
"cluster": "prefix-rewrite-1.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
"prefixRewrite": "/"
}
},
{
"match": {
"prefix": "/prefix"
},
"route": {
"cluster": "prefix-rewrite-2.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
"prefixRewrite": "/nested/newlocation"
}
},
{
"match": {
"prefix": "/timeout"
},
"route": {
"cluster": "req-timeout.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
"timeout": "33s"
}
},
{
"match": {
"prefix": "/retry-connect"
},
"route": {
"cluster": "retry-connect.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
"retryPolicy": {
"retryOn": "connect-failure",
"numRetries": 15
}
}
},
{
"match": {
"prefix": "/retry-codes"
},
"route": {
"cluster": "retry-codes.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
"retryPolicy": {
"retryOn": "retriable-status-codes",
"numRetries": 15,
"retriableStatusCodes": [
401,
409,
451
]
}
}
},
{
"match": {
"prefix": "/retry-both"
},
"route": {
"cluster": "retry-both.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
"retryPolicy": {
"retryOn": "connect-failure,retriable-status-codes",
"retriableStatusCodes": [
401,
409,
451
]
}
}
},
{
"match": {
"prefix": "/split-3-ways"
},
"route": {
"weightedClusters": {
"clusters": [
{
"name": "big-side.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
"weight": 9550
},
{
"name": "goldilocks-side.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
"weight": 400
},
{
"name": "lil-bit-side.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
"weight": 50
}
],
"totalWeight": 10000
}
}
},
{
"match": {
"prefix": "/"
},
"route": {
"cluster": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
}
],
"requestHeadersToAdd": [
{
"header": {
"key": "foo",
"value": "bar"
},
"append": true
},
{
"header": {
"key": "bar",
"value": "baz"
},
"append": false
}
],
"requestHeadersToRemove": [
"qux"
],
"responseHeadersToAdd": [
{
"header": {
"key": "foo",
"value": "bar"
},
"append": true
},
{
"header": {
"key": "bar",
"value": "baz"
},
"append": false
}
],
"responseHeadersToRemove": [
"qux"
]
}
],
"validateClusters": true
}
],
"typeUrl": "type.googleapis.com/envoy.api.v2.RouteConfiguration",
"nonce": "00000001"
}