[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:
Andrew Stucki 2023-02-13 14:18:05 -05:00 committed by GitHub
parent ab5dac3414
commit 9bb0ecfc18
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 571 additions and 58 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -181,6 +181,7 @@ func TestBoundAPIGatewayBindRoute(t *testing.T) {
Name: "Route",
Parents: []structs.ResourceReference{
{
Kind: "Foo",
Name: "Gateway",
SectionName: "Listener",
},

View File

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

View File

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

View File

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

View File

@ -0,0 +1,3 @@
#!/bin/bash
snapshot_envoy_admin localhost:20000 api-gateway primary || true

View File

@ -0,0 +1,4 @@
services {
name = "api-gateway"
kind = "api-gateway"
}

View File

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

View File

@ -0,0 +1,3 @@
#!/bin/bash
export REQUIRED_SERVICES="$DEFAULT_REQUIRED_SERVICES api-gateway-primary"

View File

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