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:
Paul Banks 2021-09-10 21:40:26 +01:00 committed by GitHub
commit b6b4080dfb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 2638 additions and 62 deletions

3
.changelog/10613.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
connect: Support manipulating HTTP headers in the mesh.
```

View File

@ -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)

View File

@ -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",
},

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

@ -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

View File

@ -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 &copy, 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 &copy, 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
}

View File

@ -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 {

View File

@ -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] {

View File

@ -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)

View File

@ -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"},
},
},
},
},

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

@ -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"`
}

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)
@ -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
}

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

@ -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": "/"

View File

@ -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": "/"

View File

@ -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

View File

@ -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

View 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"
}

View 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"
}

View File

@ -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": "/"

View File

@ -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": "/"

View File

@ -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

View File

@ -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

View File

@ -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"`
}

View File

@ -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"},
},
},
},
},

View File

@ -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"`

View File

@ -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"},
},
},
},
},

View File

@ -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"]
}
}
},
]
}
}

View File

@ -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
}

View File

@ -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"]
}
},
]
}

View File

@ -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
}

View File

@ -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"]
}
}
]
}

View File

@ -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
}