mirror of https://github.com/status-im/consul.git
[API Gateway] Add integration test for HTTP routes (#16236)
* [API Gateway] Add integration test for conflicted TCP listeners * [API Gateway] Update simple test to leverage intentions and multiple listeners * Fix broken unit test * [API Gateway] Add integration test for HTTP routes
This commit is contained in:
parent
ab5dac3414
commit
9bb0ecfc18
|
@ -65,6 +65,39 @@ func ServiceSNI(service string, subset string, namespace string, partition strin
|
|||
}
|
||||
}
|
||||
|
||||
func dotSplitLast(s string, n int) string {
|
||||
tokens := strings.SplitN(s, ".", n)
|
||||
if len(tokens) != n {
|
||||
return ""
|
||||
}
|
||||
return tokens[n-1]
|
||||
}
|
||||
|
||||
func TrustDomainForTarget(target structs.DiscoveryTarget) string {
|
||||
if target.External {
|
||||
return ""
|
||||
}
|
||||
|
||||
switch target.Partition {
|
||||
case "default":
|
||||
if target.ServiceSubset == "" {
|
||||
// service, namespace, datacenter, internal, trustDomain
|
||||
return dotSplitLast(target.SNI, 5)
|
||||
} else {
|
||||
// subset, service, namespace, datacenter, internal, trustDomain
|
||||
return dotSplitLast(target.SNI, 6)
|
||||
}
|
||||
default:
|
||||
if target.ServiceSubset == "" {
|
||||
// service, namespace, partition, datacenter, internalVersion, trustDomain
|
||||
return dotSplitLast(target.SNI, 6)
|
||||
} else {
|
||||
// subset, service, namespace, partition, datacenter, internalVersion, trustDomain
|
||||
return dotSplitLast(target.SNI, 7)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func PeeredServiceSNI(service, namespace, partition, peerName, trustDomain string) string {
|
||||
if peerName == "" {
|
||||
panic("peer name is a requirement for this function and does not make sense without it")
|
||||
|
|
|
@ -14,6 +14,8 @@ import (
|
|||
// gateway from its configuration and multiple other discovery chains.
|
||||
type GatewayChainSynthesizer struct {
|
||||
datacenter string
|
||||
trustDomain string
|
||||
suffix string
|
||||
gateway *structs.APIGatewayConfigEntry
|
||||
matchesByHostname map[string][]hostnameMatch
|
||||
tcpRoutes []structs.TCPRouteConfigEntry
|
||||
|
@ -27,9 +29,11 @@ type hostnameMatch struct {
|
|||
|
||||
// NewGatewayChainSynthesizer creates a new GatewayChainSynthesizer for the
|
||||
// given gateway and datacenter.
|
||||
func NewGatewayChainSynthesizer(datacenter string, gateway *structs.APIGatewayConfigEntry) *GatewayChainSynthesizer {
|
||||
func NewGatewayChainSynthesizer(datacenter, trustDomain, suffix string, gateway *structs.APIGatewayConfigEntry) *GatewayChainSynthesizer {
|
||||
return &GatewayChainSynthesizer{
|
||||
datacenter: datacenter,
|
||||
trustDomain: trustDomain,
|
||||
suffix: suffix,
|
||||
gateway: gateway,
|
||||
matchesByHostname: map[string][]hostnameMatch{},
|
||||
}
|
||||
|
@ -45,7 +49,13 @@ func (l *GatewayChainSynthesizer) AddTCPRoute(route structs.TCPRouteConfigEntry)
|
|||
// single hostname can be specified in multiple routes. Routing for a given
|
||||
// hostname must behave based on the aggregate of all rules that apply to it.
|
||||
func (l *GatewayChainSynthesizer) AddHTTPRoute(route structs.HTTPRouteConfigEntry) {
|
||||
for _, host := range route.Hostnames {
|
||||
hostnames := route.Hostnames
|
||||
if len(route.Hostnames) == 0 {
|
||||
// add a wildcard if there are no explicit hostnames set
|
||||
hostnames = append(hostnames, "*")
|
||||
}
|
||||
|
||||
for _, host := range hostnames {
|
||||
matches, ok := l.matchesByHostname[host]
|
||||
if !ok {
|
||||
matches = []hostnameMatch{}
|
||||
|
@ -86,41 +96,48 @@ func (l *GatewayChainSynthesizer) AddHTTPRoute(route structs.HTTPRouteConfigEntr
|
|||
// This is currently used to help API gateways masquarade as ingress gateways
|
||||
// by providing a set of virtual config entries that change the routing behavior
|
||||
// to upstreams referenced in the given HTTPRoutes or TCPRoutes.
|
||||
func (l *GatewayChainSynthesizer) Synthesize(chains ...*structs.CompiledDiscoveryChain) ([]structs.IngressService, *structs.CompiledDiscoveryChain, error) {
|
||||
func (l *GatewayChainSynthesizer) Synthesize(chains ...*structs.CompiledDiscoveryChain) ([]structs.IngressService, []*structs.CompiledDiscoveryChain, error) {
|
||||
if len(chains) == 0 {
|
||||
return nil, nil, fmt.Errorf("must provide at least one compiled discovery chain")
|
||||
}
|
||||
|
||||
services, entries := l.synthesizeEntries()
|
||||
services, set := l.synthesizeEntries()
|
||||
|
||||
if entries.IsEmpty() {
|
||||
if len(set) == 0 {
|
||||
// we can't actually compile a discovery chain, i.e. we're using a TCPRoute-based listener, instead, just return the ingresses
|
||||
// and the first pre-compiled discovery chain
|
||||
return services, chains[0], nil
|
||||
// and the pre-compiled discovery chains
|
||||
return services, chains, nil
|
||||
}
|
||||
|
||||
compiled, err := Compile(CompileRequest{
|
||||
ServiceName: l.gateway.Name,
|
||||
EvaluateInNamespace: l.gateway.NamespaceOrDefault(),
|
||||
EvaluateInPartition: l.gateway.PartitionOrDefault(),
|
||||
EvaluateInDatacenter: l.datacenter,
|
||||
Entries: entries,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
compiledChains := make([]*structs.CompiledDiscoveryChain, 0, len(set))
|
||||
for i, service := range services {
|
||||
entries := set[i]
|
||||
|
||||
for _, c := range chains {
|
||||
for id, target := range c.Targets {
|
||||
compiled.Targets[id] = target
|
||||
compiled, err := Compile(CompileRequest{
|
||||
ServiceName: service.Name,
|
||||
EvaluateInNamespace: service.NamespaceOrDefault(),
|
||||
EvaluateInPartition: service.PartitionOrDefault(),
|
||||
EvaluateInDatacenter: l.datacenter,
|
||||
EvaluateInTrustDomain: l.trustDomain,
|
||||
Entries: entries,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for id, node := range c.Nodes {
|
||||
compiled.Nodes[id] = node
|
||||
for _, c := range chains {
|
||||
for id, target := range c.Targets {
|
||||
compiled.Targets[id] = target
|
||||
}
|
||||
for id, node := range c.Nodes {
|
||||
compiled.Nodes[id] = node
|
||||
}
|
||||
compiled.EnvoyExtensions = append(compiled.EnvoyExtensions, c.EnvoyExtensions...)
|
||||
}
|
||||
compiled.EnvoyExtensions = append(compiled.EnvoyExtensions, c.EnvoyExtensions...)
|
||||
compiledChains = append(compiledChains, compiled)
|
||||
}
|
||||
|
||||
return services, compiled, nil
|
||||
return services, compiledChains, nil
|
||||
}
|
||||
|
||||
// consolidateHTTPRoutes combines all rules into the shortest possible list of routes
|
||||
|
@ -132,7 +149,7 @@ func (l *GatewayChainSynthesizer) consolidateHTTPRoutes() []structs.HTTPRouteCon
|
|||
// Create route for this hostname
|
||||
route := structs.HTTPRouteConfigEntry{
|
||||
Kind: structs.HTTPRoute,
|
||||
Name: fmt.Sprintf("%s-%s", l.gateway.Name, hostsKey(hostname)),
|
||||
Name: fmt.Sprintf("%s-%s-%s", l.gateway.Name, l.suffix, hostsKey(hostname)),
|
||||
Hostnames: []string{hostname},
|
||||
Rules: make([]structs.HTTPRouteRule, 0, len(rules)),
|
||||
Meta: l.gateway.Meta,
|
||||
|
@ -170,16 +187,18 @@ func hostsKey(hosts ...string) string {
|
|||
return strconv.FormatUint(uint64(hostsHash.Sum32()), 16)
|
||||
}
|
||||
|
||||
func (l *GatewayChainSynthesizer) synthesizeEntries() ([]structs.IngressService, *configentry.DiscoveryChainSet) {
|
||||
func (l *GatewayChainSynthesizer) synthesizeEntries() ([]structs.IngressService, []*configentry.DiscoveryChainSet) {
|
||||
services := []structs.IngressService{}
|
||||
entries := configentry.NewDiscoveryChainSet()
|
||||
entries := []*configentry.DiscoveryChainSet{}
|
||||
|
||||
for _, route := range l.consolidateHTTPRoutes() {
|
||||
entrySet := configentry.NewDiscoveryChainSet()
|
||||
ingress, router, splitters, defaults := synthesizeHTTPRouteDiscoveryChain(route)
|
||||
entries.AddRouters(router)
|
||||
entries.AddSplitters(splitters...)
|
||||
entries.AddServices(defaults...)
|
||||
entrySet.AddRouters(router)
|
||||
entrySet.AddSplitters(splitters...)
|
||||
entrySet.AddServices(defaults...)
|
||||
services = append(services, ingress)
|
||||
entries = append(entries, entrySet)
|
||||
}
|
||||
|
||||
for _, route := range l.tcpRoutes {
|
||||
|
|
|
@ -44,7 +44,7 @@ func synthesizeHTTPRouteDiscoveryChain(route structs.HTTPRouteConfigEntry) (stru
|
|||
splitters := []*structs.ServiceSplitterConfigEntry{}
|
||||
defaults := []*structs.ServiceConfigEntry{}
|
||||
|
||||
router, splits := httpRouteToDiscoveryChain(route)
|
||||
router, splits, upstreamDefaults := httpRouteToDiscoveryChain(route)
|
||||
serviceDefault := httpServiceDefault(router, meta)
|
||||
defaults = append(defaults, serviceDefault)
|
||||
for _, split := range splits {
|
||||
|
@ -53,6 +53,7 @@ func synthesizeHTTPRouteDiscoveryChain(route structs.HTTPRouteConfigEntry) (stru
|
|||
defaults = append(defaults, httpServiceDefault(split, meta))
|
||||
}
|
||||
}
|
||||
defaults = append(defaults, upstreamDefaults...)
|
||||
|
||||
ingress := structs.IngressService{
|
||||
Name: router.Name,
|
||||
|
@ -64,7 +65,7 @@ func synthesizeHTTPRouteDiscoveryChain(route structs.HTTPRouteConfigEntry) (stru
|
|||
return ingress, router, splitters, defaults
|
||||
}
|
||||
|
||||
func httpRouteToDiscoveryChain(route structs.HTTPRouteConfigEntry) (*structs.ServiceRouterConfigEntry, []*structs.ServiceSplitterConfigEntry) {
|
||||
func httpRouteToDiscoveryChain(route structs.HTTPRouteConfigEntry) (*structs.ServiceRouterConfigEntry, []*structs.ServiceSplitterConfigEntry, []*structs.ServiceConfigEntry) {
|
||||
router := &structs.ServiceRouterConfigEntry{
|
||||
Kind: structs.ServiceRouter,
|
||||
Name: route.GetName(),
|
||||
|
@ -72,6 +73,7 @@ func httpRouteToDiscoveryChain(route structs.HTTPRouteConfigEntry) (*structs.Ser
|
|||
EnterpriseMeta: route.EnterpriseMeta,
|
||||
}
|
||||
var splitters []*structs.ServiceSplitterConfigEntry
|
||||
var defaults []*structs.ServiceConfigEntry
|
||||
|
||||
for idx, rule := range route.Rules {
|
||||
modifier := httpRouteFiltersToServiceRouteHeaderModifier(rule.Filters.Headers)
|
||||
|
@ -96,6 +98,15 @@ func httpRouteToDiscoveryChain(route structs.HTTPRouteConfigEntry) (*structs.Ser
|
|||
destination.Partition = service.PartitionOrDefault()
|
||||
destination.PrefixRewrite = servicePrefixRewrite
|
||||
destination.RequestHeaders = modifier
|
||||
|
||||
// since we have already validated the protocol elsewhere, we
|
||||
// create a new service defaults here to make sure we pass validation
|
||||
defaults = append(defaults, &structs.ServiceConfigEntry{
|
||||
Kind: structs.ServiceDefaults,
|
||||
Name: service.Name,
|
||||
Protocol: "http",
|
||||
EnterpriseMeta: service.EnterpriseMeta,
|
||||
})
|
||||
} else {
|
||||
// create a virtual service to split
|
||||
destination.Service = fmt.Sprintf("%s-%d", route.GetName(), idx)
|
||||
|
@ -133,6 +144,15 @@ func httpRouteToDiscoveryChain(route structs.HTTPRouteConfigEntry) (*structs.Ser
|
|||
split.Namespace = service.NamespaceOrDefault()
|
||||
split.Partition = service.PartitionOrDefault()
|
||||
splitter.Splits = append(splitter.Splits, split)
|
||||
|
||||
// since we have already validated the protocol elsewhere, we
|
||||
// create a new service defaults here to make sure we pass validation
|
||||
defaults = append(defaults, &structs.ServiceConfigEntry{
|
||||
Kind: structs.ServiceDefaults,
|
||||
Name: service.Name,
|
||||
Protocol: "http",
|
||||
EnterpriseMeta: service.EnterpriseMeta,
|
||||
})
|
||||
}
|
||||
if len(splitter.Splits) > 0 {
|
||||
splitters = append(splitters, splitter)
|
||||
|
@ -153,7 +173,7 @@ func httpRouteToDiscoveryChain(route structs.HTTPRouteConfigEntry) (*structs.Ser
|
|||
}
|
||||
}
|
||||
|
||||
return router, splitters
|
||||
return router, splitters, defaults
|
||||
}
|
||||
|
||||
func httpRouteFiltersToDestinationPrefixRewrite(rewrites []structs.URLRewrite) string {
|
||||
|
|
|
@ -23,13 +23,15 @@ func TestGatewayChainSynthesizer_AddTCPRoute(t *testing.T) {
|
|||
expected := GatewayChainSynthesizer{
|
||||
datacenter: datacenter,
|
||||
gateway: gateway,
|
||||
trustDomain: "domain",
|
||||
suffix: "suffix",
|
||||
matchesByHostname: map[string][]hostnameMatch{},
|
||||
tcpRoutes: []structs.TCPRouteConfigEntry{
|
||||
route,
|
||||
},
|
||||
}
|
||||
|
||||
gatewayChainSynthesizer := NewGatewayChainSynthesizer(datacenter, gateway)
|
||||
gatewayChainSynthesizer := NewGatewayChainSynthesizer(datacenter, "domain", "suffix", gateway)
|
||||
|
||||
// Add a TCP route
|
||||
gatewayChainSynthesizer.AddTCPRoute(route)
|
||||
|
@ -49,7 +51,9 @@ func TestGatewayChainSynthesizer_AddHTTPRoute(t *testing.T) {
|
|||
Kind: structs.HTTPRoute,
|
||||
Name: "route",
|
||||
},
|
||||
expectedMatchesByHostname: map[string][]hostnameMatch{},
|
||||
expectedMatchesByHostname: map[string][]hostnameMatch{
|
||||
"*": {},
|
||||
},
|
||||
},
|
||||
"single hostname with no rules": {
|
||||
route: structs.HTTPRouteConfigEntry{
|
||||
|
@ -453,7 +457,7 @@ func TestGatewayChainSynthesizer_AddHTTPRoute(t *testing.T) {
|
|||
Name: "gateway",
|
||||
}
|
||||
|
||||
gatewayChainSynthesizer := NewGatewayChainSynthesizer(datacenter, gateway)
|
||||
gatewayChainSynthesizer := NewGatewayChainSynthesizer(datacenter, "domain", "suffix", gateway)
|
||||
|
||||
gatewayChainSynthesizer.AddHTTPRoute(tc.route)
|
||||
|
||||
|
@ -472,11 +476,11 @@ func TestGatewayChainSynthesizer_Synthesize(t *testing.T) {
|
|||
chain *structs.CompiledDiscoveryChain
|
||||
extra []*structs.CompiledDiscoveryChain
|
||||
expectedIngressServices []structs.IngressService
|
||||
expectedDiscoveryChain *structs.CompiledDiscoveryChain
|
||||
expectedDiscoveryChains []*structs.CompiledDiscoveryChain
|
||||
}{
|
||||
// TODO Add tests for other synthesizer types.
|
||||
"TCPRoute-based listener": {
|
||||
synthesizer: NewGatewayChainSynthesizer("dc1", &structs.APIGatewayConfigEntry{
|
||||
synthesizer: NewGatewayChainSynthesizer("dc1", "domain", "suffix", &structs.APIGatewayConfigEntry{
|
||||
Kind: structs.APIGateway,
|
||||
Name: "gateway",
|
||||
}),
|
||||
|
@ -493,14 +497,14 @@ func TestGatewayChainSynthesizer_Synthesize(t *testing.T) {
|
|||
},
|
||||
extra: []*structs.CompiledDiscoveryChain{},
|
||||
expectedIngressServices: []structs.IngressService{},
|
||||
expectedDiscoveryChain: &structs.CompiledDiscoveryChain{
|
||||
expectedDiscoveryChains: []*structs.CompiledDiscoveryChain{{
|
||||
ServiceName: "foo",
|
||||
Namespace: "default",
|
||||
Datacenter: "dc1",
|
||||
},
|
||||
}},
|
||||
},
|
||||
"HTTPRoute-based listener": {
|
||||
synthesizer: NewGatewayChainSynthesizer("dc1", &structs.APIGatewayConfigEntry{
|
||||
synthesizer: NewGatewayChainSynthesizer("dc1", "domain", "suffix", &structs.APIGatewayConfigEntry{
|
||||
Kind: structs.APIGateway,
|
||||
Name: "gateway",
|
||||
}),
|
||||
|
@ -508,6 +512,11 @@ func TestGatewayChainSynthesizer_Synthesize(t *testing.T) {
|
|||
{
|
||||
Kind: structs.HTTPRoute,
|
||||
Name: "http-route",
|
||||
Rules: []structs.HTTPRouteRule{{
|
||||
Services: []structs.HTTPService{{
|
||||
Name: "foo",
|
||||
}},
|
||||
}},
|
||||
},
|
||||
},
|
||||
chain: &structs.CompiledDiscoveryChain{
|
||||
|
@ -515,13 +524,98 @@ func TestGatewayChainSynthesizer_Synthesize(t *testing.T) {
|
|||
Namespace: "default",
|
||||
Datacenter: "dc1",
|
||||
},
|
||||
extra: []*structs.CompiledDiscoveryChain{},
|
||||
expectedIngressServices: []structs.IngressService{},
|
||||
expectedDiscoveryChain: &structs.CompiledDiscoveryChain{
|
||||
ServiceName: "foo",
|
||||
extra: []*structs.CompiledDiscoveryChain{},
|
||||
expectedIngressServices: []structs.IngressService{{
|
||||
Name: "gateway-suffix-9b9265b",
|
||||
Hosts: []string{"*"},
|
||||
}},
|
||||
expectedDiscoveryChains: []*structs.CompiledDiscoveryChain{{
|
||||
ServiceName: "gateway-suffix-9b9265b",
|
||||
Partition: "default",
|
||||
Namespace: "default",
|
||||
Datacenter: "dc1",
|
||||
},
|
||||
Protocol: "http",
|
||||
StartNode: "router:gateway-suffix-9b9265b.default.default",
|
||||
Nodes: map[string]*structs.DiscoveryGraphNode{
|
||||
"resolver:gateway-suffix-9b9265b.default.default.dc1": {
|
||||
Type: "resolver",
|
||||
Name: "gateway-suffix-9b9265b.default.default.dc1",
|
||||
Resolver: &structs.DiscoveryResolver{
|
||||
Target: "gateway-suffix-9b9265b.default.default.dc1",
|
||||
Default: true,
|
||||
ConnectTimeout: 5000000000,
|
||||
},
|
||||
},
|
||||
"router:gateway-suffix-9b9265b.default.default": {
|
||||
Type: "router",
|
||||
Name: "gateway-suffix-9b9265b.default.default",
|
||||
Routes: []*structs.DiscoveryRoute{{
|
||||
Definition: &structs.ServiceRoute{
|
||||
Match: &structs.ServiceRouteMatch{
|
||||
HTTP: &structs.ServiceRouteHTTPMatch{
|
||||
PathPrefix: "/",
|
||||
},
|
||||
},
|
||||
Destination: &structs.ServiceRouteDestination{
|
||||
Service: "foo",
|
||||
Partition: "default",
|
||||
Namespace: "default",
|
||||
RequestHeaders: &structs.HTTPHeaderModifiers{
|
||||
Add: make(map[string]string),
|
||||
Set: make(map[string]string),
|
||||
},
|
||||
},
|
||||
},
|
||||
NextNode: "resolver:foo.default.default.dc1",
|
||||
}, {
|
||||
Definition: &structs.ServiceRoute{
|
||||
Match: &structs.ServiceRouteMatch{
|
||||
HTTP: &structs.ServiceRouteHTTPMatch{
|
||||
PathPrefix: "/",
|
||||
},
|
||||
},
|
||||
Destination: &structs.ServiceRouteDestination{
|
||||
Service: "gateway-suffix-9b9265b",
|
||||
Partition: "default",
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
NextNode: "resolver:gateway-suffix-9b9265b.default.default.dc1",
|
||||
}},
|
||||
},
|
||||
"resolver:foo.default.default.dc1": {
|
||||
Type: "resolver",
|
||||
Name: "foo.default.default.dc1",
|
||||
Resolver: &structs.DiscoveryResolver{
|
||||
Target: "foo.default.default.dc1",
|
||||
Default: true,
|
||||
ConnectTimeout: 5000000000,
|
||||
},
|
||||
},
|
||||
},
|
||||
Targets: map[string]*structs.DiscoveryTarget{
|
||||
"gateway-suffix-9b9265b.default.default.dc1": {
|
||||
ID: "gateway-suffix-9b9265b.default.default.dc1",
|
||||
Service: "gateway-suffix-9b9265b",
|
||||
Datacenter: "dc1",
|
||||
Partition: "default",
|
||||
Namespace: "default",
|
||||
ConnectTimeout: 5000000000,
|
||||
SNI: "gateway-suffix-9b9265b.default.dc1.internal.domain",
|
||||
Name: "gateway-suffix-9b9265b.default.dc1.internal.domain",
|
||||
},
|
||||
"foo.default.default.dc1": {
|
||||
ID: "foo.default.default.dc1",
|
||||
Service: "foo",
|
||||
Datacenter: "dc1",
|
||||
Partition: "default",
|
||||
Namespace: "default",
|
||||
ConnectTimeout: 5000000000,
|
||||
SNI: "foo.default.dc1.internal.domain",
|
||||
Name: "foo.default.dc1.internal.domain",
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -535,11 +629,11 @@ func TestGatewayChainSynthesizer_Synthesize(t *testing.T) {
|
|||
}
|
||||
|
||||
chains := append([]*structs.CompiledDiscoveryChain{tc.chain}, tc.extra...)
|
||||
ingressServices, discoveryChain, err := tc.synthesizer.Synthesize(chains...)
|
||||
ingressServices, discoveryChains, err := tc.synthesizer.Synthesize(chains...)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.expectedIngressServices, ingressServices)
|
||||
require.Equal(t, tc.expectedDiscoveryChain, discoveryChain)
|
||||
require.Equal(t, tc.expectedDiscoveryChains, discoveryChains)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -684,7 +684,7 @@ func (g *gatewayMeta) updateRouteBinding(route structs.BoundRoute) (bool, []stru
|
|||
// shouldBindRoute returns whether a Route's parent reference references the Gateway
|
||||
// that we wrap.
|
||||
func (g *gatewayMeta) shouldBindRoute(ref structs.ResourceReference) bool {
|
||||
return ref.Kind == structs.APIGateway && g.Gateway.Name == ref.Name && g.Gateway.EnterpriseMeta.IsSame(&ref.EnterpriseMeta)
|
||||
return (ref.Kind == structs.APIGateway || ref.Kind == "") && g.Gateway.Name == ref.Name && g.Gateway.EnterpriseMeta.IsSame(&ref.EnterpriseMeta)
|
||||
}
|
||||
|
||||
// shouldBindRouteToListener returns whether a Route's parent reference should attempt
|
||||
|
|
|
@ -181,6 +181,7 @@ func TestBoundAPIGatewayBindRoute(t *testing.T) {
|
|||
Name: "Route",
|
||||
Parents: []structs.ResourceReference{
|
||||
{
|
||||
Kind: "Foo",
|
||||
Name: "Gateway",
|
||||
SectionName: "Listener",
|
||||
},
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/hashicorp/consul/acl"
|
||||
"github.com/hashicorp/consul/agent/connect"
|
||||
"github.com/hashicorp/consul/agent/consul/discoverychain"
|
||||
"github.com/hashicorp/consul/agent/proxycfg/internal/watch"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
|
@ -745,7 +746,11 @@ type configSnapshotAPIGateway struct {
|
|||
func (c *configSnapshotAPIGateway) ToIngress(datacenter string) (configSnapshotIngressGateway, error) {
|
||||
// Convert API Gateway Listeners to Ingress Listeners.
|
||||
ingressListeners := make(map[IngressListenerKey]structs.IngressListener, len(c.Listeners))
|
||||
ingressUpstreams := make(map[IngressListenerKey]structs.Upstreams, len(c.Listeners))
|
||||
synthesizedChains := map[UpstreamID]*structs.CompiledDiscoveryChain{}
|
||||
watchedUpstreamEndpoints := make(map[UpstreamID]map[string]structs.CheckServiceNodes)
|
||||
watchedGatewayEndpoints := make(map[UpstreamID]map[string]structs.CheckServiceNodes)
|
||||
|
||||
for name, listener := range c.Listeners {
|
||||
boundListener, ok := c.BoundListeners[name]
|
||||
if !ok {
|
||||
|
@ -764,14 +769,37 @@ func (c *configSnapshotAPIGateway) ToIngress(datacenter string) (configSnapshotI
|
|||
}
|
||||
|
||||
// Create a synthesized discovery chain for each service.
|
||||
services, compiled, err := c.synthesizeChains(datacenter, listener.Protocol, boundListener)
|
||||
services, upstreams, compiled, err := c.synthesizeChains(datacenter, listener.Protocol, listener.Port, listener.Name, boundListener)
|
||||
if err != nil {
|
||||
return configSnapshotIngressGateway{}, err
|
||||
}
|
||||
|
||||
if len(upstreams) == 0 {
|
||||
// skip if we can't construct any upstreams
|
||||
continue
|
||||
}
|
||||
|
||||
ingressListener.Services = services
|
||||
for _, service := range services {
|
||||
for i, service := range services {
|
||||
id := NewUpstreamIDFromServiceName(structs.NewServiceName(service.Name, &service.EnterpriseMeta))
|
||||
synthesizedChains[id] = compiled
|
||||
upstreamEndpoints := make(map[string]structs.CheckServiceNodes)
|
||||
gatewayEndpoints := make(map[string]structs.CheckServiceNodes)
|
||||
|
||||
// add the watched endpoints and gateway endpoints under the new upstream
|
||||
for _, endpoints := range c.WatchedUpstreamEndpoints {
|
||||
for targetID, endpoint := range endpoints {
|
||||
upstreamEndpoints[targetID] = endpoint
|
||||
}
|
||||
}
|
||||
for _, endpoints := range c.WatchedGatewayEndpoints {
|
||||
for targetID, endpoint := range endpoints {
|
||||
gatewayEndpoints[targetID] = endpoint
|
||||
}
|
||||
}
|
||||
|
||||
synthesizedChains[id] = compiled[i]
|
||||
watchedUpstreamEndpoints[id] = upstreamEndpoints
|
||||
watchedGatewayEndpoints[id] = gatewayEndpoints
|
||||
}
|
||||
|
||||
// Configure TLS for the ingress listener
|
||||
|
@ -786,21 +814,39 @@ func (c *configSnapshotAPIGateway) ToIngress(datacenter string) (configSnapshotI
|
|||
Protocol: string(listener.Protocol),
|
||||
}
|
||||
ingressListeners[key] = ingressListener
|
||||
ingressUpstreams[key] = upstreams
|
||||
}
|
||||
|
||||
snapshotUpstreams := c.DeepCopy().ConfigSnapshotUpstreams
|
||||
snapshotUpstreams.DiscoveryChain = synthesizedChains
|
||||
snapshotUpstreams.WatchedUpstreamEndpoints = watchedUpstreamEndpoints
|
||||
snapshotUpstreams.WatchedGatewayEndpoints = watchedGatewayEndpoints
|
||||
|
||||
return configSnapshotIngressGateway{
|
||||
Upstreams: c.Upstreams.toUpstreams(),
|
||||
Upstreams: ingressUpstreams,
|
||||
ConfigSnapshotUpstreams: snapshotUpstreams,
|
||||
GatewayConfigLoaded: true,
|
||||
Listeners: ingressListeners,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *configSnapshotAPIGateway) synthesizeChains(datacenter string, protocol structs.APIGatewayListenerProtocol, boundListener structs.BoundAPIGatewayListener) ([]structs.IngressService, *structs.CompiledDiscoveryChain, error) {
|
||||
func (c *configSnapshotAPIGateway) synthesizeChains(datacenter string, protocol structs.APIGatewayListenerProtocol, port int, name string, boundListener structs.BoundAPIGatewayListener) ([]structs.IngressService, structs.Upstreams, []*structs.CompiledDiscoveryChain, error) {
|
||||
chains := []*structs.CompiledDiscoveryChain{}
|
||||
synthesizer := discoverychain.NewGatewayChainSynthesizer(datacenter, c.GatewayConfig)
|
||||
trustDomain := ""
|
||||
|
||||
DOMAIN_LOOP:
|
||||
for _, chain := range c.DiscoveryChain {
|
||||
for _, target := range chain.Targets {
|
||||
if !target.External {
|
||||
trustDomain = connect.TrustDomainForTarget(*target)
|
||||
if trustDomain != "" {
|
||||
break DOMAIN_LOOP
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
synthesizer := discoverychain.NewGatewayChainSynthesizer(datacenter, trustDomain, name, c.GatewayConfig)
|
||||
for _, routeRef := range boundListener.Routes {
|
||||
switch routeRef.Kind {
|
||||
case structs.HTTPRoute:
|
||||
|
@ -828,15 +874,35 @@ func (c *configSnapshotAPIGateway) synthesizeChains(datacenter string, protocol
|
|||
}
|
||||
}
|
||||
default:
|
||||
return nil, nil, fmt.Errorf("unknown route kind %q", routeRef.Kind)
|
||||
return nil, nil, nil, fmt.Errorf("unknown route kind %q", routeRef.Kind)
|
||||
}
|
||||
}
|
||||
|
||||
if len(chains) == 0 {
|
||||
return nil, nil, nil
|
||||
return nil, nil, nil, nil
|
||||
}
|
||||
|
||||
return synthesizer.Synthesize(chains...)
|
||||
services, compiled, err := synthesizer.Synthesize(chains...)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
// reconstruct the upstreams
|
||||
upstreams := make([]structs.Upstream, 0, len(services))
|
||||
for _, service := range services {
|
||||
upstreams = append(upstreams, structs.Upstream{
|
||||
DestinationName: service.Name,
|
||||
DestinationNamespace: service.NamespaceOrDefault(),
|
||||
DestinationPartition: service.PartitionOrDefault(),
|
||||
IngressHosts: service.Hosts,
|
||||
LocalBindPort: port,
|
||||
Config: map[string]interface{}{
|
||||
"protocol": string(protocol),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return services, upstreams, compiled, err
|
||||
}
|
||||
|
||||
func (c *configSnapshotAPIGateway) toIngressTLS() (*structs.GatewayTLSConfig, error) {
|
||||
|
|
|
@ -68,6 +68,8 @@ func TestAPIGatewaySnapshotToIngressGatewaySnapshot(t *testing.T) {
|
|||
ConfigSnapshotUpstreams: ConfigSnapshotUpstreams{
|
||||
PeerUpstreamEndpoints: watch.NewMap[UpstreamID, structs.CheckServiceNodes](),
|
||||
WatchedLocalGWEndpoints: watch.NewMap[string, structs.CheckServiceNodes](),
|
||||
WatchedGatewayEndpoints: map[UpstreamID]map[string]structs.CheckServiceNodes{},
|
||||
WatchedUpstreamEndpoints: map[UpstreamID]map[string]structs.CheckServiceNodes{},
|
||||
UpstreamPeerTrustBundles: watch.NewMap[string, *pbpeering.PeeringTrustBundle](),
|
||||
DiscoveryChain: map[UpstreamID]*structs.CompiledDiscoveryChain{},
|
||||
},
|
||||
|
|
|
@ -71,6 +71,14 @@ type HTTPRouteConfigEntry struct {
|
|||
// Name is used to match the config entry with its associated http-route.
|
||||
Name string
|
||||
|
||||
// Parents is a list of gateways that this route should be bound to
|
||||
Parents []ResourceReference
|
||||
// Rules are a list of HTTP-based routing rules that this route should
|
||||
// use for constructing a routing table.
|
||||
Rules []HTTPRouteRule
|
||||
// Hostnames are the hostnames for which this HTTPRoute should respond to requests.
|
||||
Hostnames []string
|
||||
|
||||
Meta map[string]string `json:",omitempty"`
|
||||
|
||||
// CreateIndex is the Raft index this entry was created at. This is a
|
||||
|
@ -101,3 +109,137 @@ func (r *HTTPRouteConfigEntry) GetNamespace() string { return r.Namespace
|
|||
func (r *HTTPRouteConfigEntry) GetMeta() map[string]string { return r.Meta }
|
||||
func (r *HTTPRouteConfigEntry) GetCreateIndex() uint64 { return r.CreateIndex }
|
||||
func (r *HTTPRouteConfigEntry) GetModifyIndex() uint64 { return r.ModifyIndex }
|
||||
|
||||
// HTTPMatch specifies the criteria that should be
|
||||
// used in determining whether or not a request should
|
||||
// be routed to a given set of services.
|
||||
type HTTPMatch struct {
|
||||
Headers []HTTPHeaderMatch
|
||||
Method HTTPMatchMethod
|
||||
Path HTTPPathMatch
|
||||
Query []HTTPQueryMatch
|
||||
}
|
||||
|
||||
// HTTPMatchMethod specifies which type of HTTP verb should
|
||||
// be used for matching a given request.
|
||||
type HTTPMatchMethod string
|
||||
|
||||
const (
|
||||
HTTPMatchMethodAll HTTPMatchMethod = ""
|
||||
HTTPMatchMethodConnect HTTPMatchMethod = "CONNECT"
|
||||
HTTPMatchMethodDelete HTTPMatchMethod = "DELETE"
|
||||
HTTPMatchMethodGet HTTPMatchMethod = "GET"
|
||||
HTTPMatchMethodHead HTTPMatchMethod = "HEAD"
|
||||
HTTPMatchMethodOptions HTTPMatchMethod = "OPTIONS"
|
||||
HTTPMatchMethodPatch HTTPMatchMethod = "PATCH"
|
||||
HTTPMatchMethodPost HTTPMatchMethod = "POST"
|
||||
HTTPMatchMethodPut HTTPMatchMethod = "PUT"
|
||||
HTTPMatchMethodTrace HTTPMatchMethod = "TRACE"
|
||||
)
|
||||
|
||||
// HTTPHeaderMatchType specifies how header matching criteria
|
||||
// should be applied to a request.
|
||||
type HTTPHeaderMatchType string
|
||||
|
||||
const (
|
||||
HTTPHeaderMatchExact HTTPHeaderMatchType = "exact"
|
||||
HTTPHeaderMatchPrefix HTTPHeaderMatchType = "prefix"
|
||||
HTTPHeaderMatchPresent HTTPHeaderMatchType = "present"
|
||||
HTTPHeaderMatchRegularExpression HTTPHeaderMatchType = "regex"
|
||||
HTTPHeaderMatchSuffix HTTPHeaderMatchType = "suffix"
|
||||
)
|
||||
|
||||
// HTTPHeaderMatch specifies how a match should be done
|
||||
// on a request's headers.
|
||||
type HTTPHeaderMatch struct {
|
||||
Match HTTPHeaderMatchType
|
||||
Name string
|
||||
Value string
|
||||
}
|
||||
|
||||
// HTTPPathMatchType specifies how path matching criteria
|
||||
// should be applied to a request.
|
||||
type HTTPPathMatchType string
|
||||
|
||||
const (
|
||||
HTTPPathMatchExact HTTPPathMatchType = "exact"
|
||||
HTTPPathMatchPrefix HTTPPathMatchType = "prefix"
|
||||
HTTPPathMatchRegularExpression HTTPPathMatchType = "regex"
|
||||
)
|
||||
|
||||
// HTTPPathMatch specifies how a match should be done
|
||||
// on a request's path.
|
||||
type HTTPPathMatch struct {
|
||||
Match HTTPPathMatchType
|
||||
Value string
|
||||
}
|
||||
|
||||
// HTTPQueryMatchType specifies how querys matching criteria
|
||||
// should be applied to a request.
|
||||
type HTTPQueryMatchType string
|
||||
|
||||
const (
|
||||
HTTPQueryMatchExact HTTPQueryMatchType = "exact"
|
||||
HTTPQueryMatchPresent HTTPQueryMatchType = "present"
|
||||
HTTPQueryMatchRegularExpression HTTPQueryMatchType = "regex"
|
||||
)
|
||||
|
||||
// HTTPQueryMatch specifies how a match should be done
|
||||
// on a request's query parameters.
|
||||
type HTTPQueryMatch struct {
|
||||
Match HTTPQueryMatchType
|
||||
Name string
|
||||
Value string
|
||||
}
|
||||
|
||||
// HTTPFilters specifies a list of filters used to modify a request
|
||||
// before it is routed to an upstream.
|
||||
type HTTPFilters struct {
|
||||
Headers []HTTPHeaderFilter
|
||||
URLRewrites []URLRewrite
|
||||
}
|
||||
|
||||
// HTTPHeaderFilter specifies how HTTP headers should be modified.
|
||||
type HTTPHeaderFilter struct {
|
||||
Add map[string]string
|
||||
Remove []string
|
||||
Set map[string]string
|
||||
}
|
||||
|
||||
type URLRewrite struct {
|
||||
Path string
|
||||
}
|
||||
|
||||
// HTTPRouteRule specifies the routing rules used to determine what upstream
|
||||
// service an HTTP request is routed to.
|
||||
type HTTPRouteRule struct {
|
||||
// Filters is a list of HTTP-based filters used to modify a request prior
|
||||
// to routing it to the upstream service
|
||||
Filters HTTPFilters
|
||||
// Matches specified the matching criteria used in the routing table. If a
|
||||
// request matches the given HTTPMatch configuration, then traffic is routed
|
||||
// to services specified in the Services field.
|
||||
Matches []HTTPMatch
|
||||
// Services is a list of HTTP-based services to route to if the request matches
|
||||
// the rules specified in the Matches field.
|
||||
Services []HTTPService
|
||||
}
|
||||
|
||||
// HTTPService is a service reference for HTTP-based routing rules
|
||||
type HTTPService struct {
|
||||
Name string
|
||||
// Weight is an arbitrary integer used in calculating how much
|
||||
// traffic should be sent to the given service.
|
||||
Weight int
|
||||
// Filters is a list of HTTP-based filters used to modify a request prior
|
||||
// to routing it to the upstream service
|
||||
Filters HTTPFilters
|
||||
|
||||
// Partition is the partition the config entry is associated with.
|
||||
// Partitioning is a Consul Enterprise feature.
|
||||
Partition string `json:",omitempty"`
|
||||
|
||||
// Namespace is the namespace the config entry is associated with.
|
||||
// Namespacing is a Consul Enterprise feature.
|
||||
Namespace string `json:",omitempty"`
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/bash
|
||||
|
||||
snapshot_envoy_admin localhost:20000 api-gateway primary || true
|
|
@ -0,0 +1,4 @@
|
|||
services {
|
||||
name = "api-gateway"
|
||||
kind = "api-gateway"
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
upsert_config_entry primary '
|
||||
kind = "api-gateway"
|
||||
name = "api-gateway"
|
||||
listeners = [
|
||||
{
|
||||
name = "listener-one"
|
||||
port = 9999
|
||||
protocol = "http"
|
||||
},
|
||||
{
|
||||
name = "listener-two"
|
||||
port = 9998
|
||||
protocol = "http"
|
||||
}
|
||||
]
|
||||
'
|
||||
|
||||
upsert_config_entry primary '
|
||||
Kind = "proxy-defaults"
|
||||
Name = "global"
|
||||
Config {
|
||||
protocol = "http"
|
||||
}
|
||||
'
|
||||
|
||||
upsert_config_entry primary '
|
||||
kind = "http-route"
|
||||
name = "api-gateway-route-one"
|
||||
rules = [
|
||||
{
|
||||
services = [
|
||||
{
|
||||
name = "s1"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
parents = [
|
||||
{
|
||||
name = "api-gateway"
|
||||
sectionName = "listener-one"
|
||||
}
|
||||
]
|
||||
'
|
||||
|
||||
upsert_config_entry primary '
|
||||
kind = "http-route"
|
||||
name = "api-gateway-route-two"
|
||||
rules = [
|
||||
{
|
||||
services = [
|
||||
{
|
||||
name = "s2"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
parents = [
|
||||
{
|
||||
name = "api-gateway"
|
||||
sectionName = "listener-two"
|
||||
}
|
||||
]
|
||||
'
|
||||
|
||||
upsert_config_entry primary '
|
||||
kind = "service-intentions"
|
||||
name = "s1"
|
||||
sources {
|
||||
name = "api-gateway"
|
||||
action = "allow"
|
||||
}
|
||||
'
|
||||
|
||||
upsert_config_entry primary '
|
||||
kind = "service-intentions"
|
||||
name = "s2"
|
||||
sources {
|
||||
name = "api-gateway"
|
||||
action = "deny"
|
||||
}
|
||||
'
|
||||
|
||||
register_services primary
|
||||
|
||||
gen_envoy_bootstrap api-gateway 20000 primary true
|
||||
gen_envoy_bootstrap s1 19000
|
||||
gen_envoy_bootstrap s2 19001
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/bash
|
||||
|
||||
export REQUIRED_SERVICES="$DEFAULT_REQUIRED_SERVICES api-gateway-primary"
|
|
@ -0,0 +1,34 @@
|
|||
#!/usr/bin/env bats
|
||||
|
||||
load helpers
|
||||
|
||||
@test "api gateway proxy admin is up on :20000" {
|
||||
retry_default curl -f -s localhost:20000/stats -o /dev/null
|
||||
}
|
||||
|
||||
@test "api gateway should have be accepted and not conflicted" {
|
||||
assert_config_entry_status Accepted True Accepted primary api-gateway api-gateway
|
||||
assert_config_entry_status Conflicted False NoConflict primary api-gateway api-gateway
|
||||
}
|
||||
|
||||
@test "api gateway should have healthy endpoints for s1" {
|
||||
assert_config_entry_status Bound True Bound primary http-route api-gateway-route-one
|
||||
assert_upstream_has_endpoints_in_status 127.0.0.1:20000 s1 HEALTHY 1
|
||||
}
|
||||
|
||||
@test "api gateway should have healthy endpoints for s2" {
|
||||
assert_config_entry_status Bound True Bound primary http-route api-gateway-route-two
|
||||
assert_upstream_has_endpoints_in_status 127.0.0.1:20000 s2 HEALTHY 1
|
||||
}
|
||||
|
||||
@test "api gateway should be able to connect to s1 via configured port" {
|
||||
run retry_long curl -s -f -d hello localhost:9999
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"hello"* ]]
|
||||
}
|
||||
|
||||
@test "api gateway should get an intentions error connecting to s2 via configured port" {
|
||||
run retry_default sh -c "curl -s localhost:9998 | grep RBAC"
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == "RBAC: access denied" ]]
|
||||
}
|
Loading…
Reference in New Issue