mirror of https://github.com/status-im/consul.git
Merge pull request #10613 from hashicorp/feature/mesh-header-manip
Feature: allow manipulation of HTTP headers in ingress and mesh routing
This commit is contained in:
commit
b6b4080dfb
|
@ -0,0 +1,3 @@
|
|||
```release-note:improvement
|
||||
connect: Support manipulating HTTP headers in the mesh.
|
||||
```
|
|
@ -274,7 +274,9 @@ func (c *compiler) compile() (*structs.CompiledDiscoveryChain, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
c.flattenAdjacentSplitterNodes()
|
||||
if err := c.flattenAdjacentSplitterNodes(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := c.removeUnusedNodes(); err != nil {
|
||||
return nil, err
|
||||
|
@ -394,7 +396,7 @@ func (c *compiler) detectCircularReferences() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (c *compiler) flattenAdjacentSplitterNodes() {
|
||||
func (c *compiler) flattenAdjacentSplitterNodes() error {
|
||||
for {
|
||||
anyChanged := false
|
||||
for _, node := range c.nodes {
|
||||
|
@ -416,9 +418,16 @@ func (c *compiler) flattenAdjacentSplitterNodes() {
|
|||
for _, innerSplit := range nextNode.Splits {
|
||||
effectiveWeight := split.Weight * innerSplit.Weight / 100
|
||||
|
||||
// Copy the definition from the inner node but merge in the parent
|
||||
// to preserve any config it needs to pass through.
|
||||
newDef, err := innerSplit.Definition.MergeParent(split.Definition)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newDiscoverySplit := &structs.DiscoverySplit{
|
||||
Weight: structs.NormalizeServiceSplitWeight(effectiveWeight),
|
||||
NextNode: innerSplit.NextNode,
|
||||
Definition: newDef,
|
||||
Weight: structs.NormalizeServiceSplitWeight(effectiveWeight),
|
||||
NextNode: innerSplit.NextNode,
|
||||
}
|
||||
|
||||
fixedSplits = append(fixedSplits, newDiscoverySplit)
|
||||
|
@ -432,7 +441,7 @@ func (c *compiler) flattenAdjacentSplitterNodes() {
|
|||
}
|
||||
|
||||
if !anyChanged {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -723,9 +732,16 @@ func (c *compiler) getSplitterNode(sid structs.ServiceID) (*structs.DiscoveryGra
|
|||
c.recordNode(splitNode)
|
||||
|
||||
var hasLB bool
|
||||
for _, split := range splitter.Splits {
|
||||
for i := range splitter.Splits {
|
||||
// We don't use range variables here because we'll take the address of
|
||||
// this split and store that in a DiscoveryGraphNode and the range
|
||||
// variables share memory addresses between iterations which is exactly
|
||||
// wrong for us here.
|
||||
split := splitter.Splits[i]
|
||||
|
||||
compiledSplit := &structs.DiscoverySplit{
|
||||
Weight: split.Weight,
|
||||
Definition: &split,
|
||||
Weight: split.Weight,
|
||||
}
|
||||
splitNode.Splits = append(splitNode.Splits, compiledSplit)
|
||||
|
||||
|
|
|
@ -339,6 +339,9 @@ func testcase_RouterWithDefaults_WithNoopSplit_DefaultResolver() compileTestCase
|
|||
Name: "main.default",
|
||||
Splits: []*structs.DiscoverySplit{
|
||||
{
|
||||
Definition: &structs.ServiceSplit{
|
||||
Weight: 100,
|
||||
},
|
||||
Weight: 100,
|
||||
NextNode: "resolver:main.default.default.dc1",
|
||||
},
|
||||
|
@ -401,6 +404,9 @@ func testcase_NoopSplit_DefaultResolver_ProtocolFromProxyDefaults() compileTestC
|
|||
Name: "main.default",
|
||||
Splits: []*structs.DiscoverySplit{
|
||||
{
|
||||
Definition: &structs.ServiceSplit{
|
||||
Weight: 100,
|
||||
},
|
||||
Weight: 100,
|
||||
NextNode: "resolver:main.default.default.dc1",
|
||||
},
|
||||
|
@ -470,6 +476,9 @@ func testcase_RouterWithDefaults_WithNoopSplit_WithResolver() compileTestCase {
|
|||
Name: "main.default",
|
||||
Splits: []*structs.DiscoverySplit{
|
||||
{
|
||||
Definition: &structs.ServiceSplit{
|
||||
Weight: 100,
|
||||
},
|
||||
Weight: 100,
|
||||
NextNode: "resolver:main.default.default.dc1",
|
||||
},
|
||||
|
@ -604,6 +613,9 @@ func testcase_NoopSplit_DefaultResolver() compileTestCase {
|
|||
Name: "main.default",
|
||||
Splits: []*structs.DiscoverySplit{
|
||||
{
|
||||
Definition: &structs.ServiceSplit{
|
||||
Weight: 100,
|
||||
},
|
||||
Weight: 100,
|
||||
NextNode: "resolver:main.default.default.dc1",
|
||||
},
|
||||
|
@ -657,6 +669,9 @@ func testcase_NoopSplit_WithResolver() compileTestCase {
|
|||
Name: "main.default",
|
||||
Splits: []*structs.DiscoverySplit{
|
||||
{
|
||||
Definition: &structs.ServiceSplit{
|
||||
Weight: 100,
|
||||
},
|
||||
Weight: 100,
|
||||
NextNode: "resolver:main.default.default.dc1",
|
||||
},
|
||||
|
@ -717,10 +732,18 @@ func testcase_SubsetSplit() compileTestCase {
|
|||
Name: "main.default",
|
||||
Splits: []*structs.DiscoverySplit{
|
||||
{
|
||||
Definition: &structs.ServiceSplit{
|
||||
Weight: 60,
|
||||
ServiceSubset: "v2",
|
||||
},
|
||||
Weight: 60,
|
||||
NextNode: "resolver:v2.main.default.default.dc1",
|
||||
},
|
||||
{
|
||||
Definition: &structs.ServiceSplit{
|
||||
Weight: 40,
|
||||
ServiceSubset: "v1",
|
||||
},
|
||||
Weight: 40,
|
||||
NextNode: "resolver:v1.main.default.default.dc1",
|
||||
},
|
||||
|
@ -786,10 +809,18 @@ func testcase_ServiceSplit() compileTestCase {
|
|||
Name: "main.default",
|
||||
Splits: []*structs.DiscoverySplit{
|
||||
{
|
||||
Definition: &structs.ServiceSplit{
|
||||
Weight: 60,
|
||||
Service: "foo",
|
||||
},
|
||||
Weight: 60,
|
||||
NextNode: "resolver:foo.default.default.dc1",
|
||||
},
|
||||
{
|
||||
Definition: &structs.ServiceSplit{
|
||||
Weight: 40,
|
||||
Service: "bar",
|
||||
},
|
||||
Weight: 40,
|
||||
NextNode: "resolver:bar.default.default.dc1",
|
||||
},
|
||||
|
@ -875,6 +906,11 @@ func testcase_SplitBypassesSplit() compileTestCase {
|
|||
Name: "main.default",
|
||||
Splits: []*structs.DiscoverySplit{
|
||||
{
|
||||
Definition: &structs.ServiceSplit{
|
||||
Weight: 100,
|
||||
Service: "next",
|
||||
ServiceSubset: "bypassed",
|
||||
},
|
||||
Weight: 100,
|
||||
NextNode: "resolver:bypassed.next.default.default.dc1",
|
||||
},
|
||||
|
@ -1352,6 +1388,9 @@ func testcase_NoopSplit_WithDefaultSubset() compileTestCase {
|
|||
Name: "main.default",
|
||||
Splits: []*structs.DiscoverySplit{
|
||||
{
|
||||
Definition: &structs.ServiceSplit{
|
||||
Weight: 100,
|
||||
},
|
||||
Weight: 100,
|
||||
NextNode: "resolver:v2.main.default.default.dc1",
|
||||
},
|
||||
|
@ -1660,10 +1699,18 @@ func testcase_MultiDatacenterCanary() compileTestCase {
|
|||
Name: "main.default",
|
||||
Splits: []*structs.DiscoverySplit{
|
||||
{
|
||||
Definition: &structs.ServiceSplit{
|
||||
Weight: 60,
|
||||
Service: "main-dc2",
|
||||
},
|
||||
Weight: 60,
|
||||
NextNode: "resolver:main.default.default.dc2",
|
||||
},
|
||||
{
|
||||
Definition: &structs.ServiceSplit{
|
||||
Weight: 40,
|
||||
Service: "main-dc3",
|
||||
},
|
||||
Weight: 40,
|
||||
NextNode: "resolver:main.default.default.dc3",
|
||||
},
|
||||
|
@ -1728,7 +1775,22 @@ func testcase_AllBellsAndWhistles() compileTestCase {
|
|||
Name: "svc-split-again",
|
||||
Splits: []structs.ServiceSplit{
|
||||
{Weight: 75, Service: "main", ServiceSubset: "v1"},
|
||||
{Weight: 25, Service: "svc-split-one-more-time"},
|
||||
{
|
||||
Weight: 25,
|
||||
Service: "svc-split-one-more-time",
|
||||
RequestHeaders: &structs.HTTPHeaderModifiers{
|
||||
Set: map[string]string{
|
||||
"parent": "1",
|
||||
"shared": "from-parent",
|
||||
},
|
||||
},
|
||||
ResponseHeaders: &structs.HTTPHeaderModifiers{
|
||||
Set: map[string]string{
|
||||
"parent": "2",
|
||||
"shared": "from-parent",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&structs.ServiceSplitterConfigEntry{
|
||||
|
@ -1736,7 +1798,23 @@ func testcase_AllBellsAndWhistles() compileTestCase {
|
|||
Name: "svc-split-one-more-time",
|
||||
Splits: []structs.ServiceSplit{
|
||||
{Weight: 80, Service: "main", ServiceSubset: "v2"},
|
||||
{Weight: 20, Service: "main", ServiceSubset: "v3"},
|
||||
{
|
||||
Weight: 20,
|
||||
Service: "main",
|
||||
ServiceSubset: "v3",
|
||||
RequestHeaders: &structs.HTTPHeaderModifiers{
|
||||
Set: map[string]string{
|
||||
"child": "3",
|
||||
"shared": "from-child",
|
||||
},
|
||||
},
|
||||
ResponseHeaders: &structs.HTTPHeaderModifiers{
|
||||
Set: map[string]string{
|
||||
"child": "4",
|
||||
"shared": "from-parent",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
@ -1820,18 +1898,66 @@ func testcase_AllBellsAndWhistles() compileTestCase {
|
|||
Name: "svc-split.default",
|
||||
Splits: []*structs.DiscoverySplit{
|
||||
{
|
||||
Definition: &structs.ServiceSplit{
|
||||
Weight: 60,
|
||||
Service: "svc-redirect",
|
||||
},
|
||||
Weight: 60,
|
||||
NextNode: "resolver:prod.redirected.default.default.dc1",
|
||||
},
|
||||
{
|
||||
Definition: &structs.ServiceSplit{
|
||||
Weight: 75,
|
||||
Service: "main",
|
||||
ServiceSubset: "v1",
|
||||
},
|
||||
Weight: 30,
|
||||
NextNode: "resolver:v1.main.default.default.dc1",
|
||||
},
|
||||
{
|
||||
Definition: &structs.ServiceSplit{
|
||||
Weight: 80,
|
||||
Service: "main",
|
||||
ServiceSubset: "v2",
|
||||
// Should inherit these from parent verbatim as there was no
|
||||
// child-split header manip.
|
||||
RequestHeaders: &structs.HTTPHeaderModifiers{
|
||||
Set: map[string]string{
|
||||
"parent": "1",
|
||||
"shared": "from-parent",
|
||||
},
|
||||
},
|
||||
ResponseHeaders: &structs.HTTPHeaderModifiers{
|
||||
Set: map[string]string{
|
||||
"parent": "2",
|
||||
"shared": "from-parent",
|
||||
},
|
||||
},
|
||||
},
|
||||
Weight: 8,
|
||||
NextNode: "resolver:v2.main.default.default.dc1",
|
||||
},
|
||||
{
|
||||
Definition: &structs.ServiceSplit{
|
||||
Weight: 20,
|
||||
Service: "main",
|
||||
ServiceSubset: "v3",
|
||||
// Should get a merge of child and parent rules
|
||||
RequestHeaders: &structs.HTTPHeaderModifiers{
|
||||
Set: map[string]string{
|
||||
"parent": "1",
|
||||
"child": "3",
|
||||
"shared": "from-child",
|
||||
},
|
||||
},
|
||||
ResponseHeaders: &structs.HTTPHeaderModifiers{
|
||||
Set: map[string]string{
|
||||
"parent": "2",
|
||||
"child": "4",
|
||||
"shared": "from-parent",
|
||||
},
|
||||
},
|
||||
},
|
||||
Weight: 2,
|
||||
NextNode: "resolver:v3.main.default.default.dc1",
|
||||
},
|
||||
|
@ -2329,14 +2455,26 @@ func testcase_LBSplitterAndResolver() compileTestCase {
|
|||
Name: "main.default",
|
||||
Splits: []*structs.DiscoverySplit{
|
||||
{
|
||||
Definition: &structs.ServiceSplit{
|
||||
Weight: 60,
|
||||
Service: "foo",
|
||||
},
|
||||
Weight: 60,
|
||||
NextNode: "resolver:foo.default.default.dc1",
|
||||
},
|
||||
{
|
||||
Definition: &structs.ServiceSplit{
|
||||
Weight: 20,
|
||||
Service: "bar",
|
||||
},
|
||||
Weight: 20,
|
||||
NextNode: "resolver:bar.default.default.dc1",
|
||||
},
|
||||
{
|
||||
Definition: &structs.ServiceSplit{
|
||||
Weight: 20,
|
||||
Service: "baz",
|
||||
},
|
||||
Weight: 20,
|
||||
NextNode: "resolver:baz.default.default.dc1",
|
||||
},
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -1040,9 +1040,36 @@ func setupTestVariationConfigEntriesAndSnapshot(
|
|||
Kind: structs.ServiceSplitter,
|
||||
Name: "db",
|
||||
Splits: []structs.ServiceSplit{
|
||||
{Weight: 95.5, Service: "big-side"},
|
||||
{Weight: 4, Service: "goldilocks-side"},
|
||||
{Weight: 0.5, Service: "lil-bit-side"},
|
||||
{
|
||||
Weight: 95.5,
|
||||
Service: "big-side",
|
||||
RequestHeaders: &structs.HTTPHeaderModifiers{
|
||||
Set: map[string]string{"x-split-leg": "big"},
|
||||
},
|
||||
ResponseHeaders: &structs.HTTPHeaderModifiers{
|
||||
Set: map[string]string{"x-split-leg": "big"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Weight: 4,
|
||||
Service: "goldilocks-side",
|
||||
RequestHeaders: &structs.HTTPHeaderModifiers{
|
||||
Set: map[string]string{"x-split-leg": "goldilocks"},
|
||||
},
|
||||
ResponseHeaders: &structs.HTTPHeaderModifiers{
|
||||
Set: map[string]string{"x-split-leg": "goldilocks"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Weight: 0.5,
|
||||
Service: "lil-bit-side",
|
||||
RequestHeaders: &structs.HTTPHeaderModifiers{
|
||||
Set: map[string]string{"x-split-leg": "small"},
|
||||
},
|
||||
ResponseHeaders: &structs.HTTPHeaderModifiers{
|
||||
Set: map[string]string{"x-split-leg": "small"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
@ -1281,6 +1308,32 @@ func setupTestVariationConfigEntriesAndSnapshot(
|
|||
}),
|
||||
Destination: toService("split-3-ways"),
|
||||
},
|
||||
{
|
||||
Match: httpMatch(&structs.ServiceRouteHTTPMatch{
|
||||
PathExact: "/header-manip",
|
||||
}),
|
||||
Destination: &structs.ServiceRouteDestination{
|
||||
Service: "header-manip",
|
||||
RequestHeaders: &structs.HTTPHeaderModifiers{
|
||||
Add: map[string]string{
|
||||
"request": "bar",
|
||||
},
|
||||
Set: map[string]string{
|
||||
"bar": "baz",
|
||||
},
|
||||
Remove: []string{"qux"},
|
||||
},
|
||||
ResponseHeaders: &structs.HTTPHeaderModifiers{
|
||||
Add: map[string]string{
|
||||
"response": "bar",
|
||||
},
|
||||
Set: map[string]string{
|
||||
"bar": "baz",
|
||||
},
|
||||
Remove: []string{"qux"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
@ -1681,6 +1734,15 @@ func testConfigSnapshotIngressGateway(
|
|||
},
|
||||
},
|
||||
},
|
||||
Listeners: map[IngressListenerKey]structs.IngressListener{
|
||||
{protocol, 9191}: {
|
||||
Port: 9191,
|
||||
Protocol: protocol,
|
||||
Services: []structs.IngressService{
|
||||
{Name: "db"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
return snap
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/copystructure"
|
||||
"github.com/mitchellh/hashstructure"
|
||||
|
||||
"github.com/hashicorp/consul/acl"
|
||||
|
@ -400,6 +401,10 @@ type ServiceRouteDestination struct {
|
|||
// RetryOnStatusCodes is a flat list of http response status codes that are
|
||||
// eligible for retry. This again should be feasible in any reasonable proxy.
|
||||
RetryOnStatusCodes []uint32 `json:",omitempty" alias:"retry_on_status_codes"`
|
||||
|
||||
// Allow HTTP header manipulation to be configured.
|
||||
RequestHeaders *HTTPHeaderModifiers `json:",omitempty" alias:"request_headers"`
|
||||
ResponseHeaders *HTTPHeaderModifiers `json:",omitempty" alias:"response_headers"`
|
||||
}
|
||||
|
||||
func (e *ServiceRouteDestination) MarshalJSON() ([]byte, error) {
|
||||
|
@ -658,6 +663,83 @@ type ServiceSplit struct {
|
|||
// If this field is specified then this route is ineligible for further
|
||||
// splitting.
|
||||
Namespace string `json:",omitempty"`
|
||||
|
||||
// NOTE: Partition is not represented here by design. Do not add it.
|
||||
|
||||
// NOTE: Any configuration added to Splits that needs to be passed to the
|
||||
// proxy needs special handling MergeParent below.
|
||||
|
||||
// Allow HTTP header manipulation to be configured.
|
||||
RequestHeaders *HTTPHeaderModifiers `json:",omitempty" alias:"request_headers"`
|
||||
ResponseHeaders *HTTPHeaderModifiers `json:",omitempty" alias:"response_headers"`
|
||||
}
|
||||
|
||||
// MergeParent is called by the discovery chain compiler when a split directs to
|
||||
// another splitter. We refer to the first ServiceSplit as the parent and the
|
||||
// ServiceSplits of the second splitter as its children. The parent ends up
|
||||
// "flattened" by the compiler, i.e. replaced with its children recursively with
|
||||
// the weights modified as necessary.
|
||||
//
|
||||
// Since the parent is never included in the output, any request processing
|
||||
// config attached to it (e.g. header manipulation) would be lost and not take
|
||||
// affect when splitters direct to other splitters. To avoid that, we define a
|
||||
// MergeParent operation which is called by the compiler on each child split
|
||||
// during flattening. It must merge any request processing configuration from
|
||||
// the passed parent into the child such that the end result is equivalent to a
|
||||
// request first passing through the parent and then the child. Response
|
||||
// handling must occur as if the request first passed through the through the
|
||||
// child to the parent.
|
||||
//
|
||||
// MergeDefaults leaves both s and parent unchanged and returns a deep copy to
|
||||
// avoid confusing issues where config changes after being compiled.
|
||||
func (s *ServiceSplit) MergeParent(parent *ServiceSplit) (*ServiceSplit, error) {
|
||||
if s == nil && parent == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var err error
|
||||
var copy ServiceSplit
|
||||
|
||||
if s == nil {
|
||||
copy = *parent
|
||||
copy.RequestHeaders, err = parent.RequestHeaders.Clone()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
copy.ResponseHeaders, err = parent.ResponseHeaders.Clone()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ©, nil
|
||||
} else {
|
||||
copy = *s
|
||||
}
|
||||
|
||||
var parentReq *HTTPHeaderModifiers
|
||||
if parent != nil {
|
||||
parentReq = parent.RequestHeaders
|
||||
}
|
||||
|
||||
// Merge any request handling from parent _unless_ it's overridden by us.
|
||||
copy.RequestHeaders, err = MergeHTTPHeaderModifiers(parentReq, s.RequestHeaders)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var parentResp *HTTPHeaderModifiers
|
||||
if parent != nil {
|
||||
parentResp = parent.ResponseHeaders
|
||||
}
|
||||
|
||||
// Merge any response handling. Note that we allow parent to override this
|
||||
// time since responses flow the other way so the unflattened behavior would
|
||||
// be that the parent processing happens _after_ ours potentially overriding
|
||||
// it.
|
||||
copy.ResponseHeaders, err = MergeHTTPHeaderModifiers(s.ResponseHeaders, parentResp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ©, nil
|
||||
}
|
||||
|
||||
// ServiceResolverConfigEntry defines which instances of a service should
|
||||
|
@ -1461,3 +1543,94 @@ func IsProtocolHTTPLike(protocol string) bool {
|
|||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// HTTPHeaderModifiers is a set of rules for HTTP header modification that
|
||||
// should be performed by proxies as the request passes through them. It can
|
||||
// operate on either request or response headers depending on the context in
|
||||
// which it is used.
|
||||
type HTTPHeaderModifiers struct {
|
||||
// Add is a set of name -> value pairs that should be appended to the request
|
||||
// or response (i.e. allowing duplicates if the same header already exists).
|
||||
Add map[string]string `json:",omitempty"`
|
||||
|
||||
// Set is a set of name -> value pairs that should be added to the request or
|
||||
// response, overwriting any existing header values of the same name.
|
||||
Set map[string]string `json:",omitempty"`
|
||||
|
||||
// Remove is the set of header names that should be stripped from the request
|
||||
// or response.
|
||||
Remove []string `json:",omitempty"`
|
||||
}
|
||||
|
||||
func (m *HTTPHeaderModifiers) IsZero() bool {
|
||||
if m == nil {
|
||||
return true
|
||||
}
|
||||
return len(m.Add) == 0 && len(m.Set) == 0 && len(m.Remove) == 0
|
||||
}
|
||||
|
||||
func (m *HTTPHeaderModifiers) Validate(protocol string) error {
|
||||
if m.IsZero() {
|
||||
return nil
|
||||
}
|
||||
if !IsProtocolHTTPLike(protocol) {
|
||||
// Non nil but context is not an httpish protocol
|
||||
return fmt.Errorf("only valid for http, http2 and grpc protocols")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Clone returns a deep-copy of m unless m is nil
|
||||
func (m *HTTPHeaderModifiers) Clone() (*HTTPHeaderModifiers, error) {
|
||||
if m == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
cpy, err := copystructure.Copy(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m = cpy.(*HTTPHeaderModifiers)
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// MergeHTTPHeaderModifiers takes a base HTTPHeaderModifiers and merges in field
|
||||
// defined in overrides. Precedence is given to the overrides field if there is
|
||||
// a collision. The resulting object is returned leaving both base and overrides
|
||||
// unchanged. The `Add` field in override also replaces same-named keys of base
|
||||
// since we have no way to express multiple adds to the same key. We could
|
||||
// change that, but it makes the config syntax more complex for a huge edgecase.
|
||||
func MergeHTTPHeaderModifiers(base, overrides *HTTPHeaderModifiers) (*HTTPHeaderModifiers, error) {
|
||||
if base.IsZero() {
|
||||
return overrides.Clone()
|
||||
}
|
||||
|
||||
merged, err := base.Clone()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if overrides.IsZero() {
|
||||
return merged, nil
|
||||
}
|
||||
|
||||
for k, v := range overrides.Add {
|
||||
merged.Add[k] = v
|
||||
}
|
||||
for k, v := range overrides.Set {
|
||||
merged.Set[k] = v
|
||||
}
|
||||
|
||||
// Deduplicate removes.
|
||||
removed := make(map[string]struct{})
|
||||
for _, k := range merged.Remove {
|
||||
removed[k] = struct{}{}
|
||||
}
|
||||
for _, k := range overrides.Remove {
|
||||
if _, ok := removed[k]; !ok {
|
||||
merged.Remove = append(merged.Remove, k)
|
||||
}
|
||||
}
|
||||
|
||||
return merged, nil
|
||||
}
|
||||
|
|
|
@ -1325,6 +1325,165 @@ func TestServiceSplitterConfigEntry(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestServiceSplitMergeParent(t *testing.T) {
|
||||
|
||||
type testCase struct {
|
||||
name string
|
||||
split, parent, want *ServiceSplit
|
||||
wantErr string
|
||||
}
|
||||
|
||||
run := func(t *testing.T, tc testCase) {
|
||||
got, err := tc.split.MergeParent(tc.parent)
|
||||
if tc.wantErr != "" {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), tc.wantErr)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.want, got)
|
||||
}
|
||||
}
|
||||
|
||||
testCases := []testCase{
|
||||
{
|
||||
name: "all header manip fields set",
|
||||
split: &ServiceSplit{
|
||||
Weight: 50.0,
|
||||
Service: "foo",
|
||||
RequestHeaders: &HTTPHeaderModifiers{
|
||||
Add: map[string]string{
|
||||
"child-only": "1",
|
||||
"both-want-child": "2",
|
||||
},
|
||||
Set: map[string]string{
|
||||
"child-only": "3",
|
||||
"both-want-child": "4",
|
||||
},
|
||||
Remove: []string{"child-only-req", "both-req"},
|
||||
},
|
||||
ResponseHeaders: &HTTPHeaderModifiers{
|
||||
Add: map[string]string{
|
||||
"child-only": "5",
|
||||
"both-want-parent": "6",
|
||||
},
|
||||
Set: map[string]string{
|
||||
"child-only": "7",
|
||||
"both-want-parent": "8",
|
||||
},
|
||||
Remove: []string{"child-only-resp", "both-resp"},
|
||||
},
|
||||
},
|
||||
parent: &ServiceSplit{
|
||||
Weight: 25.0,
|
||||
Service: "bar",
|
||||
RequestHeaders: &HTTPHeaderModifiers{
|
||||
Add: map[string]string{
|
||||
"parent-only": "9",
|
||||
"both-want-child": "10",
|
||||
},
|
||||
Set: map[string]string{
|
||||
"parent-only": "11",
|
||||
"both-want-child": "12",
|
||||
},
|
||||
Remove: []string{"parent-only-req", "both-req"},
|
||||
},
|
||||
ResponseHeaders: &HTTPHeaderModifiers{
|
||||
Add: map[string]string{
|
||||
"parent-only": "13",
|
||||
"both-want-parent": "14",
|
||||
},
|
||||
Set: map[string]string{
|
||||
"parent-only": "15",
|
||||
"both-want-parent": "16",
|
||||
},
|
||||
Remove: []string{"parent-only-resp", "both-resp"},
|
||||
},
|
||||
},
|
||||
want: &ServiceSplit{
|
||||
Weight: 50.0,
|
||||
Service: "foo",
|
||||
RequestHeaders: &HTTPHeaderModifiers{
|
||||
Add: map[string]string{
|
||||
"child-only": "1",
|
||||
"both-want-child": "2",
|
||||
"parent-only": "9",
|
||||
},
|
||||
Set: map[string]string{
|
||||
"child-only": "3",
|
||||
"both-want-child": "4",
|
||||
"parent-only": "11",
|
||||
},
|
||||
Remove: []string{"parent-only-req", "both-req", "child-only-req"},
|
||||
},
|
||||
ResponseHeaders: &HTTPHeaderModifiers{
|
||||
Add: map[string]string{
|
||||
"child-only": "5",
|
||||
"parent-only": "13",
|
||||
"both-want-parent": "14",
|
||||
},
|
||||
Set: map[string]string{
|
||||
"child-only": "7",
|
||||
"parent-only": "15",
|
||||
"both-want-parent": "16",
|
||||
},
|
||||
Remove: []string{"child-only-resp", "both-resp", "parent-only-resp"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no header manip",
|
||||
split: &ServiceSplit{
|
||||
Weight: 50,
|
||||
Service: "foo",
|
||||
},
|
||||
parent: &ServiceSplit{
|
||||
Weight: 50,
|
||||
Service: "bar",
|
||||
},
|
||||
want: &ServiceSplit{
|
||||
Weight: 50,
|
||||
Service: "foo",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nil parent",
|
||||
split: &ServiceSplit{
|
||||
Weight: 50,
|
||||
Service: "foo",
|
||||
},
|
||||
parent: nil,
|
||||
want: &ServiceSplit{
|
||||
Weight: 50,
|
||||
Service: "foo",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nil child",
|
||||
split: nil,
|
||||
parent: &ServiceSplit{
|
||||
Weight: 50,
|
||||
Service: "foo",
|
||||
},
|
||||
want: &ServiceSplit{
|
||||
Weight: 50,
|
||||
Service: "foo",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "both nil",
|
||||
split: nil,
|
||||
parent: nil,
|
||||
want: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
run(t, tc)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceRouterConfigEntry(t *testing.T) {
|
||||
|
||||
httpMatch := func(http *ServiceRouteHTTPMatch) *ServiceRouteMatch {
|
||||
|
|
|
@ -75,6 +75,10 @@ type IngressService struct {
|
|||
// using a "tcp" listener.
|
||||
Hosts []string
|
||||
|
||||
// Allow HTTP header manipulation to be configured.
|
||||
RequestHeaders *HTTPHeaderModifiers `json:",omitempty" alias:"request_headers"`
|
||||
ResponseHeaders *HTTPHeaderModifiers `json:",omitempty" alias:"response_headers"`
|
||||
|
||||
Meta map[string]string `json:",omitempty"`
|
||||
EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
|
||||
}
|
||||
|
@ -164,10 +168,18 @@ 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)
|
||||
}
|
||||
sn := NewServiceName(s.Name, &s.EnterpriseMeta)
|
||||
if err := s.RequestHeaders.Validate(listener.Protocol); err != nil {
|
||||
return fmt.Errorf("request headers %s (service %q on listener on port %d)", err, sn.String(), listener.Port)
|
||||
}
|
||||
if err := s.ResponseHeaders.Validate(listener.Protocol); err != nil {
|
||||
return fmt.Errorf("response headers %s (service %q on listener on port %d)", err, sn.String(), listener.Port)
|
||||
}
|
||||
|
||||
if listener.Protocol == "tcp" {
|
||||
if s.Name == WildcardSpecifier {
|
||||
|
@ -186,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] {
|
||||
|
|
|
@ -394,6 +394,115 @@ func TestIngressGatewayConfigEntry(t *testing.T) {
|
|||
},
|
||||
validateErr: `Host '*' is not allowed when TLS is enabled, all hosts must be valid DNS records to add as a DNSSAN`,
|
||||
},
|
||||
"request header manip allowed for http(ish) protocol": {
|
||||
entry: &IngressGatewayConfigEntry{
|
||||
Kind: "ingress-gateway",
|
||||
Name: "ingress-web",
|
||||
Listeners: []IngressListener{
|
||||
{
|
||||
Port: 1111,
|
||||
Protocol: "http",
|
||||
Services: []IngressService{
|
||||
{
|
||||
Name: "web",
|
||||
RequestHeaders: &HTTPHeaderModifiers{
|
||||
Set: map[string]string{"x-foo": "bar"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Port: 2222,
|
||||
Protocol: "http2",
|
||||
Services: []IngressService{
|
||||
{
|
||||
Name: "web2",
|
||||
ResponseHeaders: &HTTPHeaderModifiers{
|
||||
Set: map[string]string{"x-foo": "bar"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Port: 3333,
|
||||
Protocol: "grpc",
|
||||
Services: []IngressService{
|
||||
{
|
||||
Name: "api",
|
||||
ResponseHeaders: &HTTPHeaderModifiers{
|
||||
Remove: []string{"x-grpc-internal"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"request header manip not allowed for non-http protocol": {
|
||||
entry: &IngressGatewayConfigEntry{
|
||||
Kind: "ingress-gateway",
|
||||
Name: "ingress-web",
|
||||
Listeners: []IngressListener{
|
||||
{
|
||||
Port: 1111,
|
||||
Protocol: "tcp",
|
||||
Services: []IngressService{
|
||||
{
|
||||
Name: "db",
|
||||
RequestHeaders: &HTTPHeaderModifiers{
|
||||
Set: map[string]string{"x-foo": "bar"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
validateErr: "request headers only valid for http",
|
||||
},
|
||||
"response header manip not allowed for non-http protocol": {
|
||||
entry: &IngressGatewayConfigEntry{
|
||||
Kind: "ingress-gateway",
|
||||
Name: "ingress-web",
|
||||
Listeners: []IngressListener{
|
||||
{
|
||||
Port: 1111,
|
||||
Protocol: "tcp",
|
||||
Services: []IngressService{
|
||||
{
|
||||
Name: "db",
|
||||
ResponseHeaders: &HTTPHeaderModifiers{
|
||||
Remove: []string{"x-foo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
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",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// Match only the last part of the exected error because the service name
|
||||
// differs between Ent and OSS default/default/web vs web
|
||||
validateErr: "cannot be added multiple times (listener on port 1111)",
|
||||
},
|
||||
}
|
||||
|
||||
testConfigEntryNormalizeAndValidate(t, cases)
|
||||
|
|
|
@ -463,6 +463,24 @@ func TestDecodeConfigEntry(t *testing.T) {
|
|||
num_retries = 12345
|
||||
retry_on_connect_failure = true
|
||||
retry_on_status_codes = [401, 209]
|
||||
request_headers {
|
||||
add {
|
||||
x-foo = "bar"
|
||||
}
|
||||
set {
|
||||
bar = "baz"
|
||||
}
|
||||
remove = ["qux"]
|
||||
}
|
||||
response_headers {
|
||||
add {
|
||||
x-foo = "bar"
|
||||
}
|
||||
set {
|
||||
bar = "baz"
|
||||
}
|
||||
remove = ["qux"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -546,6 +564,24 @@ func TestDecodeConfigEntry(t *testing.T) {
|
|||
NumRetries = 12345
|
||||
RetryOnConnectFailure = true
|
||||
RetryOnStatusCodes = [401, 209]
|
||||
RequestHeaders {
|
||||
Add {
|
||||
x-foo = "bar"
|
||||
}
|
||||
Set {
|
||||
bar = "baz"
|
||||
}
|
||||
Remove = ["qux"]
|
||||
}
|
||||
ResponseHeaders {
|
||||
Add {
|
||||
x-foo = "bar"
|
||||
}
|
||||
Set {
|
||||
bar = "baz"
|
||||
}
|
||||
Remove = ["qux"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -629,6 +665,16 @@ func TestDecodeConfigEntry(t *testing.T) {
|
|||
NumRetries: 12345,
|
||||
RetryOnConnectFailure: true,
|
||||
RetryOnStatusCodes: []uint32{401, 209},
|
||||
RequestHeaders: &HTTPHeaderModifiers{
|
||||
Add: map[string]string{"x-foo": "bar"},
|
||||
Set: map[string]string{"bar": "baz"},
|
||||
Remove: []string{"qux"},
|
||||
},
|
||||
ResponseHeaders: &HTTPHeaderModifiers{
|
||||
Add: map[string]string{"x-foo": "bar"},
|
||||
Set: map[string]string{"bar": "baz"},
|
||||
Remove: []string{"qux"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -674,13 +720,31 @@ func TestDecodeConfigEntry(t *testing.T) {
|
|||
}
|
||||
splits = [
|
||||
{
|
||||
weight = 99.1
|
||||
service_subset = "v1"
|
||||
weight = 99.1
|
||||
service_subset = "v1"
|
||||
request_headers {
|
||||
add {
|
||||
foo = "bar"
|
||||
}
|
||||
set {
|
||||
bar = "baz"
|
||||
}
|
||||
remove = ["qux"]
|
||||
}
|
||||
response_headers {
|
||||
add {
|
||||
foo = "bar"
|
||||
}
|
||||
set {
|
||||
bar = "baz"
|
||||
}
|
||||
remove = ["qux"]
|
||||
}
|
||||
},
|
||||
{
|
||||
weight = 0.9
|
||||
service = "other"
|
||||
namespace = "alt"
|
||||
weight = 0.9
|
||||
service = "other"
|
||||
namespace = "alt"
|
||||
},
|
||||
]
|
||||
`,
|
||||
|
@ -693,13 +757,31 @@ func TestDecodeConfigEntry(t *testing.T) {
|
|||
}
|
||||
Splits = [
|
||||
{
|
||||
Weight = 99.1
|
||||
ServiceSubset = "v1"
|
||||
Weight = 99.1
|
||||
ServiceSubset = "v1"
|
||||
RequestHeaders {
|
||||
Add {
|
||||
foo = "bar"
|
||||
}
|
||||
Set {
|
||||
bar = "baz"
|
||||
}
|
||||
Remove = ["qux"]
|
||||
}
|
||||
ResponseHeaders {
|
||||
Add {
|
||||
foo = "bar"
|
||||
}
|
||||
Set {
|
||||
bar = "baz"
|
||||
}
|
||||
Remove = ["qux"]
|
||||
}
|
||||
},
|
||||
{
|
||||
Weight = 0.9
|
||||
Service = "other"
|
||||
Namespace = "alt"
|
||||
Weight = 0.9
|
||||
Service = "other"
|
||||
Namespace = "alt"
|
||||
},
|
||||
]
|
||||
`,
|
||||
|
@ -714,6 +796,16 @@ func TestDecodeConfigEntry(t *testing.T) {
|
|||
{
|
||||
Weight: 99.1,
|
||||
ServiceSubset: "v1",
|
||||
RequestHeaders: &HTTPHeaderModifiers{
|
||||
Add: map[string]string{"foo": "bar"},
|
||||
Set: map[string]string{"bar": "baz"},
|
||||
Remove: []string{"qux"},
|
||||
},
|
||||
ResponseHeaders: &HTTPHeaderModifiers{
|
||||
Add: map[string]string{"foo": "bar"},
|
||||
Set: map[string]string{"bar": "baz"},
|
||||
Remove: []string{"qux"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Weight: 0.9,
|
||||
|
@ -1037,6 +1129,24 @@ func TestDecodeConfigEntry(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name = "db"
|
||||
request_headers {
|
||||
add {
|
||||
foo = "bar"
|
||||
}
|
||||
set {
|
||||
bar = "baz"
|
||||
}
|
||||
remove = ["qux"]
|
||||
}
|
||||
response_headers {
|
||||
add {
|
||||
foo = "bar"
|
||||
}
|
||||
set {
|
||||
bar = "baz"
|
||||
}
|
||||
remove = ["qux"]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -1081,6 +1191,24 @@ func TestDecodeConfigEntry(t *testing.T) {
|
|||
},
|
||||
{
|
||||
Name = "db"
|
||||
RequestHeaders {
|
||||
Add {
|
||||
foo = "bar"
|
||||
}
|
||||
Set {
|
||||
bar = "baz"
|
||||
}
|
||||
Remove = ["qux"]
|
||||
}
|
||||
ResponseHeaders {
|
||||
Add {
|
||||
foo = "bar"
|
||||
}
|
||||
Set {
|
||||
bar = "baz"
|
||||
}
|
||||
Remove = ["qux"]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -1125,6 +1253,16 @@ func TestDecodeConfigEntry(t *testing.T) {
|
|||
},
|
||||
{
|
||||
Name: "db",
|
||||
RequestHeaders: &HTTPHeaderModifiers{
|
||||
Add: map[string]string{"foo": "bar"},
|
||||
Set: map[string]string{"bar": "baz"},
|
||||
Remove: []string{"qux"},
|
||||
},
|
||||
ResponseHeaders: &HTTPHeaderModifiers{
|
||||
Add: map[string]string{"foo": "bar"},
|
||||
Set: map[string]string{"bar": "baz"},
|
||||
Remove: []string{"qux"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -192,6 +192,13 @@ type DiscoveryRoute struct {
|
|||
|
||||
// compiled form of ServiceSplit
|
||||
type DiscoverySplit struct {
|
||||
Definition *ServiceSplit `json:",omitempty"`
|
||||
// Weight is not necessarily a duplicate of Definition.Weight since when
|
||||
// multiple splits are compiled down to a single set of splits the effective
|
||||
// weight of a split leg might not be the same as in the original definition.
|
||||
// Proxies should use this compiled weight. The Definition is provided above
|
||||
// for any other significant configuration that the proxy might need to apply
|
||||
// to that leg of the split.
|
||||
Weight float32 `json:",omitempty"`
|
||||
NextNode string `json:",omitempty"`
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
@ -283,24 +355,23 @@ func makeUpstreamRouteForDiscoveryChain(
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if err := injectLBToRouteAction(lb, routeAction.Route); err != nil {
|
||||
return nil, fmt.Errorf("failed to apply load balancer configuration to route action: %v", err)
|
||||
}
|
||||
|
||||
case structs.DiscoveryGraphNodeTypeResolver:
|
||||
routeAction = makeRouteActionForChainCluster(nextNode.Resolver.Target, chain)
|
||||
|
||||
if err := injectLBToRouteAction(lb, routeAction.Route); err != nil {
|
||||
return nil, fmt.Errorf("failed to apply load balancer configuration to route action: %v", err)
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected graph node after route %q", nextNode.Type)
|
||||
}
|
||||
|
||||
if err := injectLBToRouteAction(lb, routeAction.Route); err != nil {
|
||||
return nil, fmt.Errorf("failed to apply load balancer configuration to route action: %v", err)
|
||||
}
|
||||
|
||||
// TODO(rb): Better help handle the envoy case where you need (prefix=/foo/,rewrite=/) and (exact=/foo,rewrite=/) to do a full rewrite
|
||||
|
||||
destination := discoveryRoute.Definition.Destination
|
||||
|
||||
route := &envoy_route_v3.Route{}
|
||||
|
||||
if destination != nil {
|
||||
if destination.PrefixRewrite != "" {
|
||||
routeAction.Route.PrefixRewrite = destination.PrefixRewrite
|
||||
|
@ -331,12 +402,16 @@ func makeUpstreamRouteForDiscoveryChain(
|
|||
|
||||
routeAction.Route.RetryPolicy = retryPolicy
|
||||
}
|
||||
|
||||
if err := injectHeaderManipToRoute(destination, route); err != nil {
|
||||
return nil, fmt.Errorf("failed to apply header manipulation configuration to route: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
routes = append(routes, &envoy_route_v3.Route{
|
||||
Match: routeMatch,
|
||||
Action: routeAction,
|
||||
})
|
||||
route.Match = routeMatch
|
||||
route.Action = routeAction
|
||||
|
||||
routes = append(routes, route)
|
||||
}
|
||||
|
||||
case structs.DiscoveryGraphNodeTypeSplitter:
|
||||
|
@ -558,6 +633,9 @@ func makeRouteActionForSplitter(splits []*structs.DiscoverySplit, chain *structs
|
|||
Weight: makeUint32Value(int(split.Weight * 100)),
|
||||
Name: clusterName,
|
||||
}
|
||||
if err := injectHeaderManipToWeightedCluster(split.Definition, cw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
clusters = append(clusters, cw)
|
||||
}
|
||||
|
@ -642,3 +720,67 @@ func injectLBToRouteAction(lb *structs.LoadBalancer, action *envoy_route_v3.Rout
|
|||
action.HashPolicy = result
|
||||
return nil
|
||||
}
|
||||
|
||||
func injectHeaderManipToRoute(dest *structs.ServiceRouteDestination, r *envoy_route_v3.Route) error {
|
||||
if !dest.RequestHeaders.IsZero() {
|
||||
r.RequestHeadersToAdd = append(
|
||||
r.RequestHeadersToAdd,
|
||||
makeHeadersValueOptions(dest.RequestHeaders.Add, true)...,
|
||||
)
|
||||
r.RequestHeadersToAdd = append(
|
||||
r.RequestHeadersToAdd,
|
||||
makeHeadersValueOptions(dest.RequestHeaders.Set, false)...,
|
||||
)
|
||||
r.RequestHeadersToRemove = append(
|
||||
r.RequestHeadersToRemove,
|
||||
dest.RequestHeaders.Remove...,
|
||||
)
|
||||
}
|
||||
if !dest.ResponseHeaders.IsZero() {
|
||||
r.ResponseHeadersToAdd = append(
|
||||
r.ResponseHeadersToAdd,
|
||||
makeHeadersValueOptions(dest.ResponseHeaders.Add, true)...,
|
||||
)
|
||||
r.ResponseHeadersToAdd = append(
|
||||
r.ResponseHeadersToAdd,
|
||||
makeHeadersValueOptions(dest.ResponseHeaders.Set, false)...,
|
||||
)
|
||||
r.ResponseHeadersToRemove = append(
|
||||
r.ResponseHeadersToRemove,
|
||||
dest.ResponseHeaders.Remove...,
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func injectHeaderManipToWeightedCluster(split *structs.ServiceSplit, c *envoy_route_v3.WeightedCluster_ClusterWeight) error {
|
||||
if !split.RequestHeaders.IsZero() {
|
||||
c.RequestHeadersToAdd = append(
|
||||
c.RequestHeadersToAdd,
|
||||
makeHeadersValueOptions(split.RequestHeaders.Add, true)...,
|
||||
)
|
||||
c.RequestHeadersToAdd = append(
|
||||
c.RequestHeadersToAdd,
|
||||
makeHeadersValueOptions(split.RequestHeaders.Set, false)...,
|
||||
)
|
||||
c.RequestHeadersToRemove = append(
|
||||
c.RequestHeadersToRemove,
|
||||
split.RequestHeaders.Remove...,
|
||||
)
|
||||
}
|
||||
if !split.ResponseHeaders.IsZero() {
|
||||
c.ResponseHeadersToAdd = append(
|
||||
c.ResponseHeadersToAdd,
|
||||
makeHeadersValueOptions(split.ResponseHeaders.Add, true)...,
|
||||
)
|
||||
c.ResponseHeadersToAdd = append(
|
||||
c.ResponseHeadersToAdd,
|
||||
makeHeadersValueOptions(split.ResponseHeaders.Set, false)...,
|
||||
)
|
||||
c.ResponseHeadersToRemove = append(
|
||||
c.ResponseHeadersToRemove,
|
||||
split.ResponseHeaders.Remove...,
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -343,6 +343,52 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": {
|
||||
"path": "/header-manip"
|
||||
},
|
||||
"route": {
|
||||
"cluster": "header-manip.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
|
||||
},
|
||||
"requestHeadersToAdd": [
|
||||
{
|
||||
"header": {
|
||||
"key": "request",
|
||||
"value": "bar"
|
||||
},
|
||||
"append": true
|
||||
},
|
||||
{
|
||||
"header": {
|
||||
"key": "bar",
|
||||
"value": "baz"
|
||||
},
|
||||
"append": false
|
||||
}
|
||||
],
|
||||
"requestHeadersToRemove": [
|
||||
"qux"
|
||||
],
|
||||
"responseHeadersToAdd": [
|
||||
{
|
||||
"header": {
|
||||
"key": "response",
|
||||
"value": "bar"
|
||||
},
|
||||
"append": true
|
||||
},
|
||||
{
|
||||
"header": {
|
||||
"key": "bar",
|
||||
"value": "baz"
|
||||
},
|
||||
"append": false
|
||||
}
|
||||
],
|
||||
"responseHeadersToRemove": [
|
||||
"qux"
|
||||
]
|
||||
},
|
||||
{
|
||||
"match": {
|
||||
"prefix": "/"
|
||||
|
|
|
@ -343,6 +343,52 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": {
|
||||
"path": "/header-manip"
|
||||
},
|
||||
"route": {
|
||||
"cluster": "header-manip.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
|
||||
},
|
||||
"requestHeadersToAdd": [
|
||||
{
|
||||
"header": {
|
||||
"key": "request",
|
||||
"value": "bar"
|
||||
},
|
||||
"append": true
|
||||
},
|
||||
{
|
||||
"header": {
|
||||
"key": "bar",
|
||||
"value": "baz"
|
||||
},
|
||||
"append": false
|
||||
}
|
||||
],
|
||||
"requestHeadersToRemove": [
|
||||
"qux"
|
||||
],
|
||||
"responseHeadersToAdd": [
|
||||
{
|
||||
"header": {
|
||||
"key": "response",
|
||||
"value": "bar"
|
||||
},
|
||||
"append": true
|
||||
},
|
||||
{
|
||||
"header": {
|
||||
"key": "bar",
|
||||
"value": "baz"
|
||||
},
|
||||
"append": false
|
||||
}
|
||||
],
|
||||
"responseHeadersToRemove": [
|
||||
"qux"
|
||||
]
|
||||
},
|
||||
{
|
||||
"match": {
|
||||
"prefix": "/"
|
||||
|
|
|
@ -20,15 +20,69 @@
|
|||
"clusters": [
|
||||
{
|
||||
"name": "big-side.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
|
||||
"weight": 9550
|
||||
"weight": 9550,
|
||||
"requestHeadersToAdd": [
|
||||
{
|
||||
"header": {
|
||||
"key": "x-split-leg",
|
||||
"value": "big"
|
||||
},
|
||||
"append": false
|
||||
}
|
||||
],
|
||||
"responseHeadersToAdd": [
|
||||
{
|
||||
"header": {
|
||||
"key": "x-split-leg",
|
||||
"value": "big"
|
||||
},
|
||||
"append": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "goldilocks-side.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
|
||||
"weight": 400
|
||||
"weight": 400,
|
||||
"requestHeadersToAdd": [
|
||||
{
|
||||
"header": {
|
||||
"key": "x-split-leg",
|
||||
"value": "goldilocks"
|
||||
},
|
||||
"append": false
|
||||
}
|
||||
],
|
||||
"responseHeadersToAdd": [
|
||||
{
|
||||
"header": {
|
||||
"key": "x-split-leg",
|
||||
"value": "goldilocks"
|
||||
},
|
||||
"append": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "lil-bit-side.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
|
||||
"weight": 50
|
||||
"weight": 50,
|
||||
"requestHeadersToAdd": [
|
||||
{
|
||||
"header": {
|
||||
"key": "x-split-leg",
|
||||
"value": "small"
|
||||
},
|
||||
"append": false
|
||||
}
|
||||
],
|
||||
"responseHeadersToAdd": [
|
||||
{
|
||||
"header": {
|
||||
"key": "x-split-leg",
|
||||
"value": "small"
|
||||
},
|
||||
"append": false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"totalWeight": 10000
|
||||
|
|
|
@ -20,15 +20,69 @@
|
|||
"clusters": [
|
||||
{
|
||||
"name": "big-side.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
|
||||
"weight": 9550
|
||||
"weight": 9550,
|
||||
"requestHeadersToAdd": [
|
||||
{
|
||||
"header": {
|
||||
"key": "x-split-leg",
|
||||
"value": "big"
|
||||
},
|
||||
"append": false
|
||||
}
|
||||
],
|
||||
"responseHeadersToAdd": [
|
||||
{
|
||||
"header": {
|
||||
"key": "x-split-leg",
|
||||
"value": "big"
|
||||
},
|
||||
"append": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "goldilocks-side.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
|
||||
"weight": 400
|
||||
"weight": 400,
|
||||
"requestHeadersToAdd": [
|
||||
{
|
||||
"header": {
|
||||
"key": "x-split-leg",
|
||||
"value": "goldilocks"
|
||||
},
|
||||
"append": false
|
||||
}
|
||||
],
|
||||
"responseHeadersToAdd": [
|
||||
{
|
||||
"header": {
|
||||
"key": "x-split-leg",
|
||||
"value": "goldilocks"
|
||||
},
|
||||
"append": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "lil-bit-side.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
|
||||
"weight": 50
|
||||
"weight": 50,
|
||||
"requestHeadersToAdd": [
|
||||
{
|
||||
"header": {
|
||||
"key": "x-split-leg",
|
||||
"value": "small"
|
||||
},
|
||||
"append": false
|
||||
}
|
||||
],
|
||||
"responseHeadersToAdd": [
|
||||
{
|
||||
"header": {
|
||||
"key": "x-split-leg",
|
||||
"value": "small"
|
||||
},
|
||||
"append": false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"totalWeight": 10000
|
||||
|
|
447
agent/xds/testdata/routes/ingress-with-chain-and-router-header-manip.envoy-1-18-x.golden
vendored
Normal file
447
agent/xds/testdata/routes/ingress-with-chain-and-router-header-manip.envoy-1-18-x.golden
vendored
Normal file
|
@ -0,0 +1,447 @@
|
|||
{
|
||||
"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": {
|
||||
"path": "/header-manip"
|
||||
},
|
||||
"route": {
|
||||
"cluster": "header-manip.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
|
||||
},
|
||||
"requestHeadersToAdd": [
|
||||
{
|
||||
"header": {
|
||||
"key": "request",
|
||||
"value": "bar"
|
||||
},
|
||||
"append": true
|
||||
},
|
||||
{
|
||||
"header": {
|
||||
"key": "bar",
|
||||
"value": "baz"
|
||||
},
|
||||
"append": false
|
||||
}
|
||||
],
|
||||
"requestHeadersToRemove": [
|
||||
"qux"
|
||||
],
|
||||
"responseHeadersToAdd": [
|
||||
{
|
||||
"header": {
|
||||
"key": "response",
|
||||
"value": "bar"
|
||||
},
|
||||
"append": true
|
||||
},
|
||||
{
|
||||
"header": {
|
||||
"key": "bar",
|
||||
"value": "baz"
|
||||
},
|
||||
"append": false
|
||||
}
|
||||
],
|
||||
"responseHeadersToRemove": [
|
||||
"qux"
|
||||
]
|
||||
},
|
||||
{
|
||||
"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"
|
||||
}
|
447
agent/xds/testdata/routes/ingress-with-chain-and-router-header-manip.v2compat.envoy-1-16-x.golden
vendored
Normal file
447
agent/xds/testdata/routes/ingress-with-chain-and-router-header-manip.v2compat.envoy-1-16-x.golden
vendored
Normal file
|
@ -0,0 +1,447 @@
|
|||
{
|
||||
"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": {
|
||||
"path": "/header-manip"
|
||||
},
|
||||
"route": {
|
||||
"cluster": "header-manip.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
|
||||
},
|
||||
"requestHeadersToAdd": [
|
||||
{
|
||||
"header": {
|
||||
"key": "request",
|
||||
"value": "bar"
|
||||
},
|
||||
"append": true
|
||||
},
|
||||
{
|
||||
"header": {
|
||||
"key": "bar",
|
||||
"value": "baz"
|
||||
},
|
||||
"append": false
|
||||
}
|
||||
],
|
||||
"requestHeadersToRemove": [
|
||||
"qux"
|
||||
],
|
||||
"responseHeadersToAdd": [
|
||||
{
|
||||
"header": {
|
||||
"key": "response",
|
||||
"value": "bar"
|
||||
},
|
||||
"append": true
|
||||
},
|
||||
{
|
||||
"header": {
|
||||
"key": "bar",
|
||||
"value": "baz"
|
||||
},
|
||||
"append": false
|
||||
}
|
||||
],
|
||||
"responseHeadersToRemove": [
|
||||
"qux"
|
||||
]
|
||||
},
|
||||
{
|
||||
"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"
|
||||
}
|
|
@ -344,6 +344,52 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": {
|
||||
"path": "/header-manip"
|
||||
},
|
||||
"route": {
|
||||
"cluster": "header-manip.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
|
||||
},
|
||||
"requestHeadersToAdd": [
|
||||
{
|
||||
"header": {
|
||||
"key": "request",
|
||||
"value": "bar"
|
||||
},
|
||||
"append": true
|
||||
},
|
||||
{
|
||||
"header": {
|
||||
"key": "bar",
|
||||
"value": "baz"
|
||||
},
|
||||
"append": false
|
||||
}
|
||||
],
|
||||
"requestHeadersToRemove": [
|
||||
"qux"
|
||||
],
|
||||
"responseHeadersToAdd": [
|
||||
{
|
||||
"header": {
|
||||
"key": "response",
|
||||
"value": "bar"
|
||||
},
|
||||
"append": true
|
||||
},
|
||||
{
|
||||
"header": {
|
||||
"key": "bar",
|
||||
"value": "baz"
|
||||
},
|
||||
"append": false
|
||||
}
|
||||
],
|
||||
"responseHeadersToRemove": [
|
||||
"qux"
|
||||
]
|
||||
},
|
||||
{
|
||||
"match": {
|
||||
"prefix": "/"
|
||||
|
|
|
@ -344,6 +344,52 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": {
|
||||
"path": "/header-manip"
|
||||
},
|
||||
"route": {
|
||||
"cluster": "header-manip.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
|
||||
},
|
||||
"requestHeadersToAdd": [
|
||||
{
|
||||
"header": {
|
||||
"key": "request",
|
||||
"value": "bar"
|
||||
},
|
||||
"append": true
|
||||
},
|
||||
{
|
||||
"header": {
|
||||
"key": "bar",
|
||||
"value": "baz"
|
||||
},
|
||||
"append": false
|
||||
}
|
||||
],
|
||||
"requestHeadersToRemove": [
|
||||
"qux"
|
||||
],
|
||||
"responseHeadersToAdd": [
|
||||
{
|
||||
"header": {
|
||||
"key": "response",
|
||||
"value": "bar"
|
||||
},
|
||||
"append": true
|
||||
},
|
||||
{
|
||||
"header": {
|
||||
"key": "bar",
|
||||
"value": "baz"
|
||||
},
|
||||
"append": false
|
||||
}
|
||||
],
|
||||
"responseHeadersToRemove": [
|
||||
"qux"
|
||||
]
|
||||
},
|
||||
{
|
||||
"match": {
|
||||
"prefix": "/"
|
||||
|
|
|
@ -21,15 +21,69 @@
|
|||
"clusters": [
|
||||
{
|
||||
"name": "big-side.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
|
||||
"weight": 9550
|
||||
"weight": 9550,
|
||||
"requestHeadersToAdd": [
|
||||
{
|
||||
"header": {
|
||||
"key": "x-split-leg",
|
||||
"value": "big"
|
||||
},
|
||||
"append": false
|
||||
}
|
||||
],
|
||||
"responseHeadersToAdd": [
|
||||
{
|
||||
"header": {
|
||||
"key": "x-split-leg",
|
||||
"value": "big"
|
||||
},
|
||||
"append": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "goldilocks-side.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
|
||||
"weight": 400
|
||||
"weight": 400,
|
||||
"requestHeadersToAdd": [
|
||||
{
|
||||
"header": {
|
||||
"key": "x-split-leg",
|
||||
"value": "goldilocks"
|
||||
},
|
||||
"append": false
|
||||
}
|
||||
],
|
||||
"responseHeadersToAdd": [
|
||||
{
|
||||
"header": {
|
||||
"key": "x-split-leg",
|
||||
"value": "goldilocks"
|
||||
},
|
||||
"append": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "lil-bit-side.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
|
||||
"weight": 50
|
||||
"weight": 50,
|
||||
"requestHeadersToAdd": [
|
||||
{
|
||||
"header": {
|
||||
"key": "x-split-leg",
|
||||
"value": "small"
|
||||
},
|
||||
"append": false
|
||||
}
|
||||
],
|
||||
"responseHeadersToAdd": [
|
||||
{
|
||||
"header": {
|
||||
"key": "x-split-leg",
|
||||
"value": "small"
|
||||
},
|
||||
"append": false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"totalWeight": 10000
|
||||
|
|
|
@ -21,15 +21,69 @@
|
|||
"clusters": [
|
||||
{
|
||||
"name": "big-side.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
|
||||
"weight": 9550
|
||||
"weight": 9550,
|
||||
"requestHeadersToAdd": [
|
||||
{
|
||||
"header": {
|
||||
"key": "x-split-leg",
|
||||
"value": "big"
|
||||
},
|
||||
"append": false
|
||||
}
|
||||
],
|
||||
"responseHeadersToAdd": [
|
||||
{
|
||||
"header": {
|
||||
"key": "x-split-leg",
|
||||
"value": "big"
|
||||
},
|
||||
"append": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "goldilocks-side.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
|
||||
"weight": 400
|
||||
"weight": 400,
|
||||
"requestHeadersToAdd": [
|
||||
{
|
||||
"header": {
|
||||
"key": "x-split-leg",
|
||||
"value": "goldilocks"
|
||||
},
|
||||
"append": false
|
||||
}
|
||||
],
|
||||
"responseHeadersToAdd": [
|
||||
{
|
||||
"header": {
|
||||
"key": "x-split-leg",
|
||||
"value": "goldilocks"
|
||||
},
|
||||
"append": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "lil-bit-side.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
|
||||
"weight": 50
|
||||
"weight": 50,
|
||||
"requestHeadersToAdd": [
|
||||
{
|
||||
"header": {
|
||||
"key": "x-split-leg",
|
||||
"value": "small"
|
||||
},
|
||||
"append": false
|
||||
}
|
||||
],
|
||||
"responseHeadersToAdd": [
|
||||
{
|
||||
"header": {
|
||||
"key": "x-split-leg",
|
||||
"value": "small"
|
||||
},
|
||||
"append": false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"totalWeight": 10000
|
||||
|
|
|
@ -61,14 +61,16 @@ type ServiceRouteHTTPMatchQueryParam struct {
|
|||
}
|
||||
|
||||
type ServiceRouteDestination struct {
|
||||
Service string `json:",omitempty"`
|
||||
ServiceSubset string `json:",omitempty" alias:"service_subset"`
|
||||
Namespace string `json:",omitempty"`
|
||||
PrefixRewrite string `json:",omitempty" alias:"prefix_rewrite"`
|
||||
RequestTimeout time.Duration `json:",omitempty" alias:"request_timeout"`
|
||||
NumRetries uint32 `json:",omitempty" alias:"num_retries"`
|
||||
RetryOnConnectFailure bool `json:",omitempty" alias:"retry_on_connect_failure"`
|
||||
RetryOnStatusCodes []uint32 `json:",omitempty" alias:"retry_on_status_codes"`
|
||||
Service string `json:",omitempty"`
|
||||
ServiceSubset string `json:",omitempty" alias:"service_subset"`
|
||||
Namespace string `json:",omitempty"`
|
||||
PrefixRewrite string `json:",omitempty" alias:"prefix_rewrite"`
|
||||
RequestTimeout time.Duration `json:",omitempty" alias:"request_timeout"`
|
||||
NumRetries uint32 `json:",omitempty" alias:"num_retries"`
|
||||
RetryOnConnectFailure bool `json:",omitempty" alias:"retry_on_connect_failure"`
|
||||
RetryOnStatusCodes []uint32 `json:",omitempty" alias:"retry_on_status_codes"`
|
||||
RequestHeaders *HTTPHeaderModifiers `json:",omitempty" alias:"request_headers"`
|
||||
ResponseHeaders *HTTPHeaderModifiers `json:",omitempty" alias:"response_headers"`
|
||||
}
|
||||
|
||||
func (e *ServiceRouteDestination) MarshalJSON() ([]byte, error) {
|
||||
|
@ -127,10 +129,12 @@ func (e *ServiceSplitterConfigEntry) GetCreateIndex() uint64 { return e.Crea
|
|||
func (e *ServiceSplitterConfigEntry) GetModifyIndex() uint64 { return e.ModifyIndex }
|
||||
|
||||
type ServiceSplit struct {
|
||||
Weight float32
|
||||
Service string `json:",omitempty"`
|
||||
ServiceSubset string `json:",omitempty" alias:"service_subset"`
|
||||
Namespace string `json:",omitempty"`
|
||||
Weight float32
|
||||
Service string `json:",omitempty"`
|
||||
ServiceSubset string `json:",omitempty" alias:"service_subset"`
|
||||
Namespace string `json:",omitempty"`
|
||||
RequestHeaders *HTTPHeaderModifiers `json:",omitempty" alias:"request_headers"`
|
||||
ResponseHeaders *HTTPHeaderModifiers `json:",omitempty" alias:"response_headers"`
|
||||
}
|
||||
|
||||
type ServiceResolverConfigEntry struct {
|
||||
|
@ -287,3 +291,21 @@ type CookieConfig struct {
|
|||
// The path to set for the cookie
|
||||
Path string `json:",omitempty"`
|
||||
}
|
||||
|
||||
// HTTPHeaderModifiers is a set of rules for HTTP header modification that
|
||||
// should be performed by proxies as the request passes through them. It can
|
||||
// operate on either request or response headers depending on the context in
|
||||
// which it is used.
|
||||
type HTTPHeaderModifiers struct {
|
||||
// Add is a set of name -> value pairs that should be appended to the request
|
||||
// or response (i.e. allowing duplicates if the same header already exists).
|
||||
Add map[string]string `json:",omitempty"`
|
||||
|
||||
// Set is a set of name -> value pairs that should be added to the request or
|
||||
// response, overwriting any existing header values of the same name.
|
||||
Set map[string]string `json:",omitempty"`
|
||||
|
||||
// Remove is the set of header names that should be stripped from the request
|
||||
// or response.
|
||||
Remove []string `json:",omitempty"`
|
||||
}
|
||||
|
|
|
@ -193,6 +193,14 @@ func TestAPI_ConfigEntry_DiscoveryChain(t *testing.T) {
|
|||
Service: "test-failover",
|
||||
ServiceSubset: "v1",
|
||||
Namespace: defaultNamespace,
|
||||
RequestHeaders: &HTTPHeaderModifiers{
|
||||
Set: map[string]string{
|
||||
"x-foo": "bar",
|
||||
},
|
||||
},
|
||||
ResponseHeaders: &HTTPHeaderModifiers{
|
||||
Remove: []string{"x-foo"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Weight: 10,
|
||||
|
@ -235,6 +243,14 @@ func TestAPI_ConfigEntry_DiscoveryChain(t *testing.T) {
|
|||
NumRetries: 5,
|
||||
RetryOnConnectFailure: true,
|
||||
RetryOnStatusCodes: []uint32{500, 503, 401},
|
||||
RequestHeaders: &HTTPHeaderModifiers{
|
||||
Set: map[string]string{
|
||||
"x-foo": "bar",
|
||||
},
|
||||
},
|
||||
ResponseHeaders: &HTTPHeaderModifiers{
|
||||
Remove: []string{"x-foo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -83,6 +83,10 @@ type IngressService struct {
|
|||
// using a "tcp" listener.
|
||||
Hosts []string
|
||||
|
||||
// Allow HTTP header manipulation to be configured.
|
||||
RequestHeaders *HTTPHeaderModifiers `json:",omitempty" alias:"request_headers"`
|
||||
ResponseHeaders *HTTPHeaderModifiers `json:",omitempty" alias:"response_headers"`
|
||||
|
||||
// Namespace is the namespace where the service is located.
|
||||
// Namespacing is a Consul Enterprise feature.
|
||||
Namespace string `json:",omitempty"`
|
||||
|
|
|
@ -78,6 +78,14 @@ func TestAPI_ConfigEntries_IngressGateway(t *testing.T) {
|
|||
{
|
||||
Name: "asdf",
|
||||
Hosts: []string{"test.example.com"},
|
||||
RequestHeaders: &HTTPHeaderModifiers{
|
||||
Set: map[string]string{
|
||||
"x-foo": "bar",
|
||||
},
|
||||
},
|
||||
ResponseHeaders: &HTTPHeaderModifiers{
|
||||
Remove: []string{"x-foo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -291,6 +291,36 @@ config_entries {
|
|||
prefix_rewrite = "/debug"
|
||||
}
|
||||
},
|
||||
{
|
||||
match { http {
|
||||
path_exact = "/header-manip/debug"
|
||||
} },
|
||||
destination {
|
||||
service_subset = "v2"
|
||||
prefix_rewrite = "/debug"
|
||||
request_headers {
|
||||
set {
|
||||
x-foo = "request-bar"
|
||||
}
|
||||
remove = ["x-bad-req"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
match { http {
|
||||
path_exact = "/header-manip/echo"
|
||||
} },
|
||||
destination {
|
||||
service_subset = "v2"
|
||||
prefix_rewrite = "/"
|
||||
response_headers {
|
||||
add {
|
||||
x-foo = "response-bar"
|
||||
}
|
||||
remove = ["x-bad-resp"]
|
||||
}
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -104,3 +104,44 @@ load helpers
|
|||
@test "test method match" {
|
||||
assert_expected_fortio_name s2-v2 localhost 5000 /method-match
|
||||
}
|
||||
|
||||
@test "test request header manipulation" {
|
||||
run retry_default curl -s -f \
|
||||
-H "X-Bad-Req: true" \
|
||||
"localhost:5000/header-manip/debug?env=dump"
|
||||
|
||||
echo "GOT: $output"
|
||||
|
||||
[ "$status" == "0" ]
|
||||
|
||||
# Should have been routed to the right server
|
||||
echo "$output" | grep -E "^FORTIO_NAME=s2-v2"
|
||||
|
||||
# Route should have added the right request header
|
||||
echo "$output" | grep -E "^X-Foo: request-bar"
|
||||
|
||||
# Route should have removed the bad request header
|
||||
if echo "$output" | grep -E "^X-Bad-Req: true"; then
|
||||
echo "X-Bad-Req request header should have been stripped but was still present"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
@test "test response header manipulation" {
|
||||
# Add a response header that should be stripped by the route.
|
||||
run retry_default curl -v -f -X PUT \
|
||||
"localhost:5000/header-manip/echo?header=x-bad-resp:true"
|
||||
|
||||
echo "GOT: $output"
|
||||
|
||||
[ "$status" == "0" ]
|
||||
|
||||
# Route should have added the right response header (this is output by curl -v)
|
||||
echo "$output" | grep -E "^< x-foo: response-bar"
|
||||
|
||||
# Route should have removed the bad response header
|
||||
if echo "$output" | grep -E "^< x-bad-resp: true"; then
|
||||
echo "X-Bad-Resp response header should have been stripped but was still present"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
|
|
@ -31,10 +31,34 @@ config_entries {
|
|||
{
|
||||
weight = 50,
|
||||
service_subset = "v2"
|
||||
request_headers {
|
||||
set {
|
||||
x-split-leg = "v2"
|
||||
}
|
||||
remove = ["x-bad-req"]
|
||||
}
|
||||
response_headers {
|
||||
add {
|
||||
x-svc-version = "v2"
|
||||
}
|
||||
remove = ["x-bad-resp"]
|
||||
}
|
||||
},
|
||||
{
|
||||
weight = 50,
|
||||
service_subset = "v1"
|
||||
request_headers {
|
||||
set {
|
||||
x-split-leg = "v1"
|
||||
}
|
||||
remove = ["x-bad-req"]
|
||||
}
|
||||
response_headers {
|
||||
add {
|
||||
x-svc-version = "v1"
|
||||
}
|
||||
remove = ["x-bad-resp"]
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
|
|
|
@ -50,3 +50,48 @@ load helpers
|
|||
@test "s1 upstream should be able to connect to s2-v1 or s2-v2 via upstream s2" {
|
||||
assert_expected_fortio_name_pattern ^FORTIO_NAME=s2-v[12]$
|
||||
}
|
||||
|
||||
@test "test request header manipulation" {
|
||||
run retry_default curl -s -f \
|
||||
-H "X-Bad-Req: true" \
|
||||
"localhost:5000/debug?env=dump"
|
||||
|
||||
|
||||
echo "GOT: $output"
|
||||
|
||||
[ "$status" == "0" ]
|
||||
|
||||
# Figure out which version we hit. This will fail the test if the grep can't
|
||||
# find a match while capturing the v1 or v2 from the server name in VERSION
|
||||
VERSION=$(echo "$output" | grep -o -E "^FORTIO_NAME=s2-v[12]" | grep -o 'v[12]$')
|
||||
|
||||
# Route should have added the right request header
|
||||
GOT_HEADER=$(echo "$output" | grep -E "^X-Split-Leg: v[12]" | grep -o 'v[12]$')
|
||||
|
||||
[ "$GOT_HEADER" == "$VERSION" ]
|
||||
|
||||
# Route should have removed the bad request header
|
||||
if echo "$output" | grep -E "^X-Bad-Req: true"; then
|
||||
echo "X-Bad-Req request header should have been stripped but was still present"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
@test "test response header manipulation" {
|
||||
# Add a response header that should be stripped by the route.
|
||||
run retry_default curl -v -f -X PUT \
|
||||
"localhost:5000/header-manip/echo?header=x-bad-resp:true"
|
||||
|
||||
echo "GOT: $output"
|
||||
|
||||
[ "$status" == "0" ]
|
||||
|
||||
# Splitter should have added the right response header (this is output by curl -v)
|
||||
echo "$output" | grep -E "^< x-svc-version: v[12]"
|
||||
|
||||
# Splitter should have removed the bad response header
|
||||
if echo "$output" | grep -E "^< x-bad-resp: true"; then
|
||||
echo "X-Bad-Resp response header should have been stripped but was still present"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
|
|
@ -18,6 +18,27 @@ config_entries {
|
|||
services = [
|
||||
{
|
||||
name = "router"
|
||||
request_headers {
|
||||
add {
|
||||
x-foo = "bar-req"
|
||||
x-existing-1 = "appended-req"
|
||||
}
|
||||
set {
|
||||
x-existing-2 = "replaced-req"
|
||||
x-client-ip = "%DOWNSTREAM_REMOTE_ADDRESS_WITHOUT_PORT%"
|
||||
}
|
||||
remove = ["x-bad-req"]
|
||||
}
|
||||
response_headers {
|
||||
add {
|
||||
x-foo = "bar-resp"
|
||||
x-existing-1 = "appended-resp"
|
||||
}
|
||||
set {
|
||||
x-existing-2 = "replaced-resp"
|
||||
}
|
||||
remove = ["x-bad-resp"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -38,3 +38,69 @@ load helpers
|
|||
assert_expected_fortio_name s2 router.ingress.consul 9999 /s2
|
||||
}
|
||||
|
||||
@test "test request header manipulation" {
|
||||
run retry_default curl -s -f \
|
||||
-H "Host: router.ingress.consul" \
|
||||
-H "X-Existing-1: original" \
|
||||
-H "X-Existing-2: original" \
|
||||
-H "X-Bad-Req: true" \
|
||||
"localhost:9999/s2/debug?env=dump"
|
||||
|
||||
echo "GOT: $output"
|
||||
|
||||
[ "$status" == "0" ]
|
||||
|
||||
# Should have been routed to the right server
|
||||
echo "$output" | grep -E "^FORTIO_NAME=s2"
|
||||
|
||||
# Ingress should have added the new request header
|
||||
echo "$output" | grep -E "^X-Foo: bar-req"
|
||||
|
||||
# Ingress should have appended the first existing header - both should be
|
||||
# present
|
||||
echo "$output" | grep -E "^X-Existing-1: original,appended-req"
|
||||
|
||||
# Ingress should have replaced the second existing header
|
||||
echo "$output" | grep -E "^X-Existing-2: replaced-req"
|
||||
|
||||
# Ingress should have set the client ip from dynamic Envoy variable
|
||||
echo "$output" | grep -E "^X-Client-Ip: 127.0.0.1"
|
||||
|
||||
# Ingress should have removed the bad request header
|
||||
if echo "$output" | grep -E "^X-Bad-Req: true"; then
|
||||
echo "X-Bad-Req request header should have been stripped but was still present"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
@test "test response header manipulation" {
|
||||
# Add a response header that should be stripped by the route.
|
||||
run retry_default curl -v -s -f -X PUT \
|
||||
-H "Host: router.ingress.consul" \
|
||||
"localhost:9999/s2/echo?header=x-bad-resp:true&header=x-existing-1:original&header=x-existing-2:original"
|
||||
|
||||
echo "GOT: $output"
|
||||
|
||||
[ "$status" == "0" ]
|
||||
|
||||
# Ingress should have added the new response header
|
||||
echo "$output" | grep -E "^< x-foo: bar-resp"
|
||||
|
||||
# Ingress should have appended the first existing header - both should be
|
||||
# present
|
||||
echo "$output" | grep -E "^< x-existing-1: original"
|
||||
echo "$output" | grep -E "^< x-existing-1: appended-resp"
|
||||
|
||||
# Ingress should have replaced the second existing header
|
||||
echo "$output" | grep -E "^< x-existing-2: replaced-resp"
|
||||
if echo "$output" | grep -E "^< x-existing-2: original"; then
|
||||
echo "x-existing-2 response header should have been overridden, original still present"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Ingress should have removed the bad response header
|
||||
if echo "$output" | grep -E "^< x-bad-resp: true"; then
|
||||
echo "X-Bad-Resp response header should have been stripped but was still present"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue