Implement BindRoutesToGateways (#15950)

* Stub out bind code
* Move into a new package and flesh out binding
* Fill in the actual binding logic
* Bind to all listeners if not specified
* Move bind code up to gateways package
* Fix resource type check
* Add UpsertRoute to listeners
* Add RemoveRoute to listener
* Implement binding as associated functions
* Pass in gateways to BindRouteToGateways
* Add a bunch of tests
* Fix hopping from one listener on a gateway to another
* Remove parents from HTTPRoute
* Apply suggestions from code review
* Fix merge conflict
* Unify binding into a single variadic function 🙌 @nathancoleman
* Remove vestigial error
* Add TODO on protocol check
This commit is contained in:
Thomas Eckert 2023-01-20 15:11:16 -05:00 committed by GitHub
parent ada3530213
commit b01dca96af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 971 additions and 0 deletions

View File

@ -0,0 +1,75 @@
package gateways
import (
"errors"
"github.com/hashicorp/consul/agent/configentry"
"github.com/hashicorp/consul/agent/structs"
)
// referenceSet stores an O(1) accessible set of ResourceReference objects.
type referenceSet = map[structs.ResourceReference]any
// gatewayRefs maps a gateway kind/name to a set of resource references.
type gatewayRefs = map[configentry.KindName][]structs.ResourceReference
// BindRoutesToGateways takes a slice of bound API gateways and a variadic number of routes.
// It iterates over the parent references for each route. These parents are gateways the
// route should be bound to. If the parent matches a bound gateway, the route is bound to the
// gateway. Otherwise, the route is unbound from the gateway if it was previously bound.
//
// The function returns a list of references to the modified BoundAPIGatewayConfigEntry objects,
// a map of resource references to errors that occurred when they were attempted to be
// bound to a gateway.
func BindRoutesToGateways(gateways []*structs.BoundAPIGatewayConfigEntry, routes ...structs.BoundRoute) ([]*structs.BoundAPIGatewayConfigEntry, map[structs.ResourceReference]error) {
modified := make([]*structs.BoundAPIGatewayConfigEntry, 0, len(gateways))
// errored stores the errors from events where a resource reference failed to bind to a gateway.
errored := make(map[structs.ResourceReference]error)
for _, route := range routes {
parentRefs, gatewayRefs := getReferences(route)
// Iterate over all BoundAPIGateway config entries and try to bind them to the route if they are a parent.
for _, gateway := range gateways {
references, routeReferencesGateway := gatewayRefs[configentry.NewKindNameForEntry(gateway)]
if routeReferencesGateway {
didUpdate, errors := gateway.UpdateRouteBinding(references, route)
if didUpdate {
modified = append(modified, gateway)
}
for ref, err := range errors {
errored[ref] = err
}
for _, ref := range references {
delete(parentRefs, ref)
}
} else {
if gateway.UnbindRoute(route) {
modified = append(modified, gateway)
}
}
}
// Add all references that aren't bound at this point to the error set.
for reference := range parentRefs {
errored[reference] = errors.New("invalid reference to missing parent")
}
}
return modified, errored
}
// getReferences returns a set of all the resource references for a given route as well as
// a map of gateway kind/name to a list of resource references for that gateway.
func getReferences(route structs.BoundRoute) (referenceSet, gatewayRefs) {
parentRefs := make(referenceSet)
gatewayRefs := make(gatewayRefs)
for _, ref := range route.GetParents() {
parentRefs[ref] = struct{}{}
kindName := configentry.NewKindName(structs.BoundAPIGateway, ref.Name, &ref.EnterpriseMeta)
gatewayRefs[kindName] = append(gatewayRefs[kindName], ref)
}
return parentRefs, gatewayRefs
}

View File

@ -0,0 +1,305 @@
package gateways
import (
"testing"
"github.com/hashicorp/consul/agent/structs"
"github.com/stretchr/testify/require"
)
func TestBindRoutesToGateways(t *testing.T) {
type testCase struct {
gateways []*structs.BoundAPIGatewayConfigEntry
routes []structs.BoundRoute
expectedBoundAPIGateways []*structs.BoundAPIGatewayConfigEntry
expectedReferenceErrors map[structs.ResourceReference]error
}
cases := map[string]testCase{
"TCP Route binds to gateway": {
gateways: []*structs.BoundAPIGatewayConfigEntry{
makeGateway("Test Bound API Gateway", []structs.BoundAPIGatewayListener{
makeListener("Test Listener", []structs.ResourceReference{}),
}),
},
routes: []structs.BoundRoute{
makeRoute(structs.TCPRoute, "Test TCP Route", []structs.ResourceReference{
makeRef(structs.APIGateway, "Test Bound API Gateway", "Test Listener"),
}),
},
expectedBoundAPIGateways: []*structs.BoundAPIGatewayConfigEntry{
makeGateway("Test Bound API Gateway", []structs.BoundAPIGatewayListener{
makeListener("Test Listener", []structs.ResourceReference{
makeRef(structs.TCPRoute, "Test TCP Route", ""),
}),
}),
},
expectedReferenceErrors: map[structs.ResourceReference]error{},
},
"TCP Route unbinds from gateway": {
gateways: []*structs.BoundAPIGatewayConfigEntry{
makeGateway("Test Bound API Gateway", []structs.BoundAPIGatewayListener{
makeListener("Test Listener", []structs.ResourceReference{
makeRef(structs.TCPRoute, "Test TCP Route", ""),
}),
}),
makeGateway("Other Test Bound API Gateway", []structs.BoundAPIGatewayListener{
makeListener("Test Listener", []structs.ResourceReference{
makeRef(structs.TCPRoute, "Test TCP Route", ""),
}),
}),
},
routes: []structs.BoundRoute{
makeRoute(structs.TCPRoute, "Test TCP Route", []structs.ResourceReference{
makeRef(structs.APIGateway, "Test Bound API Gateway", "Test Listener"),
}),
},
expectedBoundAPIGateways: []*structs.BoundAPIGatewayConfigEntry{
makeGateway("Test Bound API Gateway", []structs.BoundAPIGatewayListener{
makeListener("Test Listener", []structs.ResourceReference{
makeRef(structs.TCPRoute, "Test TCP Route", ""),
}),
}),
makeGateway("Other Test Bound API Gateway", []structs.BoundAPIGatewayListener{
makeListener("Test Listener", []structs.ResourceReference{}),
}),
},
expectedReferenceErrors: map[structs.ResourceReference]error{},
},
"TCP Route binds to multiple gateways": {
gateways: []*structs.BoundAPIGatewayConfigEntry{
makeGateway("Test Bound API Gateway", []structs.BoundAPIGatewayListener{
makeListener("Test Listener", []structs.ResourceReference{}),
}),
makeGateway("Other Test Bound API Gateway", []structs.BoundAPIGatewayListener{
makeListener("Test Listener", []structs.ResourceReference{}),
}),
},
routes: []structs.BoundRoute{
makeRoute(structs.TCPRoute, "Test TCP Route", []structs.ResourceReference{
makeRef(structs.APIGateway, "Test Bound API Gateway", "Test Listener"),
makeRef(structs.APIGateway, "Other Test Bound API Gateway", "Test Listener"),
}),
},
expectedBoundAPIGateways: []*structs.BoundAPIGatewayConfigEntry{
makeGateway("Test Bound API Gateway", []structs.BoundAPIGatewayListener{
makeListener("Test Listener", []structs.ResourceReference{
makeRef(structs.TCPRoute, "Test TCP Route", ""),
}),
}),
makeGateway("Other Test Bound API Gateway", []structs.BoundAPIGatewayListener{
makeListener("Test Listener", []structs.ResourceReference{
makeRef(structs.TCPRoute, "Test TCP Route", ""),
}),
}),
},
expectedReferenceErrors: map[structs.ResourceReference]error{},
},
"TCP Route binds to gateway with multiple listeners": {
gateways: []*structs.BoundAPIGatewayConfigEntry{
makeGateway("Test Bound API Gateway", []structs.BoundAPIGatewayListener{
makeListener("Test Listener", []structs.ResourceReference{}),
makeListener("Other Test Listener", []structs.ResourceReference{}),
}),
},
routes: []structs.BoundRoute{
makeRoute(structs.TCPRoute, "Test TCP Route", []structs.ResourceReference{
makeRef(structs.APIGateway, "Test Bound API Gateway", "Test Listener"),
}),
},
expectedBoundAPIGateways: []*structs.BoundAPIGatewayConfigEntry{
makeGateway("Test Bound API Gateway", []structs.BoundAPIGatewayListener{
makeListener("Test Listener", []structs.ResourceReference{
makeRef(structs.TCPRoute, "Test TCP Route", ""),
}),
makeListener("Other Test Listener", []structs.ResourceReference{}),
}),
},
expectedReferenceErrors: map[structs.ResourceReference]error{},
},
"TCP Route binds to all listeners on a gateway": {
gateways: []*structs.BoundAPIGatewayConfigEntry{
makeGateway("Test Bound API Gateway", []structs.BoundAPIGatewayListener{
makeListener("Test Listener", []structs.ResourceReference{}),
makeListener("Other Test Listener", []structs.ResourceReference{}),
}),
},
routes: []structs.BoundRoute{
makeRoute(structs.TCPRoute, "Test TCP Route", []structs.ResourceReference{
makeRef(structs.APIGateway, "Test Bound API Gateway", ""),
}),
},
expectedBoundAPIGateways: []*structs.BoundAPIGatewayConfigEntry{
makeGateway("Test Bound API Gateway", []structs.BoundAPIGatewayListener{
makeListener("Test Listener", []structs.ResourceReference{
makeRef(structs.TCPRoute, "Test TCP Route", ""),
}),
makeListener("Other Test Listener", []structs.ResourceReference{
makeRef(structs.TCPRoute, "Test TCP Route", ""),
}),
}),
},
expectedReferenceErrors: map[structs.ResourceReference]error{},
},
"TCP Route binds to gateway with multiple listeners, one of which is already bound": {
gateways: []*structs.BoundAPIGatewayConfigEntry{
makeGateway("Test Bound API Gateway", []structs.BoundAPIGatewayListener{
makeListener("Test Listener", []structs.ResourceReference{}),
makeListener("Other Test Listener", []structs.ResourceReference{
makeRef(structs.TCPRoute, "Test TCP Route", ""),
}),
}),
},
routes: []structs.BoundRoute{
makeRoute(structs.TCPRoute, "Test TCP Route", []structs.ResourceReference{
makeRef(structs.APIGateway, "Test Bound API Gateway", "Test Listener"),
makeRef(structs.APIGateway, "Test Bound API Gateway", "Other Test Listener"),
}),
},
expectedBoundAPIGateways: []*structs.BoundAPIGatewayConfigEntry{
makeGateway("Test Bound API Gateway", []structs.BoundAPIGatewayListener{
makeListener("Test Listener", []structs.ResourceReference{
makeRef(structs.TCPRoute, "Test TCP Route", ""),
}),
makeListener("Other Test Listener", []structs.ResourceReference{
makeRef(structs.TCPRoute, "Test TCP Route", ""),
}),
}),
},
expectedReferenceErrors: map[structs.ResourceReference]error{},
},
"TCP Route binds to a listener on multiple gateways": {
gateways: []*structs.BoundAPIGatewayConfigEntry{
makeGateway("Test Bound API Gateway", []structs.BoundAPIGatewayListener{
makeListener("Test Listener", []structs.ResourceReference{}),
makeListener("Other Test Listener", []structs.ResourceReference{}),
}),
makeGateway("Other Test Bound API Gateway", []structs.BoundAPIGatewayListener{
makeListener("Test Listener", []structs.ResourceReference{}),
makeListener("Other Test Listener", []structs.ResourceReference{}),
}),
},
routes: []structs.BoundRoute{
makeRoute(structs.TCPRoute, "Test TCP Route", []structs.ResourceReference{
makeRef(structs.APIGateway, "Test Bound API Gateway", "Test Listener"),
makeRef(structs.APIGateway, "Other Test Bound API Gateway", "Test Listener"),
}),
},
expectedBoundAPIGateways: []*structs.BoundAPIGatewayConfigEntry{
makeGateway("Test Bound API Gateway", []structs.BoundAPIGatewayListener{
makeListener("Test Listener", []structs.ResourceReference{
makeRef(structs.TCPRoute, "Test TCP Route", ""),
}),
makeListener("Other Test Listener", []structs.ResourceReference{}),
}),
makeGateway("Other Test Bound API Gateway", []structs.BoundAPIGatewayListener{
makeListener("Test Listener", []structs.ResourceReference{
makeRef(structs.TCPRoute, "Test TCP Route", ""),
}),
makeListener("Other Test Listener", []structs.ResourceReference{}),
}),
},
expectedReferenceErrors: map[structs.ResourceReference]error{},
},
"TCP Route swaps from one listener to another on a gateway": {
gateways: []*structs.BoundAPIGatewayConfigEntry{
makeGateway("Test Bound API Gateway", []structs.BoundAPIGatewayListener{
makeListener("Test Listener", []structs.ResourceReference{
makeRef(structs.TCPRoute, "Test TCP Route", ""),
}),
makeListener("Other Test Listener", []structs.ResourceReference{}),
}),
},
routes: []structs.BoundRoute{
makeRoute(structs.TCPRoute, "Test TCP Route", []structs.ResourceReference{
makeRef(structs.APIGateway, "Test Bound API Gateway", "Other Test Listener"),
}),
},
expectedBoundAPIGateways: []*structs.BoundAPIGatewayConfigEntry{
makeGateway("Test Bound API Gateway", []structs.BoundAPIGatewayListener{
makeListener("Test Listener", []structs.ResourceReference{}),
makeListener("Other Test Listener", []structs.ResourceReference{
makeRef(structs.TCPRoute, "Test TCP Route", ""),
}),
}),
},
expectedReferenceErrors: map[structs.ResourceReference]error{},
},
"TCP Routes bind to each gateway": {
gateways: []*structs.BoundAPIGatewayConfigEntry{
makeGateway("Test Bound API Gateway", []structs.BoundAPIGatewayListener{
makeListener("Test Listener", []structs.ResourceReference{}),
}),
makeGateway("Other Test Bound API Gateway", []structs.BoundAPIGatewayListener{
makeListener("Other Test Listener", []structs.ResourceReference{}),
}),
},
routes: []structs.BoundRoute{
makeRoute(structs.TCPRoute, "Test TCP Route", []structs.ResourceReference{
makeRef(structs.APIGateway, "Test Bound API Gateway", "Test Listener"),
}),
makeRoute(structs.TCPRoute, "Other Test TCP Route", []structs.ResourceReference{
makeRef(structs.APIGateway, "Other Test Bound API Gateway", "Other Test Listener"),
}),
},
expectedBoundAPIGateways: []*structs.BoundAPIGatewayConfigEntry{
makeGateway("Test Bound API Gateway", []structs.BoundAPIGatewayListener{
makeListener("Test Listener", []structs.ResourceReference{
makeRef(structs.TCPRoute, "Test TCP Route", ""),
}),
}),
makeGateway("Other Test Bound API Gateway", []structs.BoundAPIGatewayListener{
makeListener("Other Test Listener", []structs.ResourceReference{
makeRef(structs.TCPRoute, "Other Test TCP Route", ""),
}),
}),
},
expectedReferenceErrors: map[structs.ResourceReference]error{},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
actualBoundAPIGateways, referenceErrors := BindRoutesToGateways(tc.gateways, tc.routes...)
require.Equal(t, tc.expectedBoundAPIGateways, actualBoundAPIGateways)
require.Equal(t, tc.expectedReferenceErrors, referenceErrors)
})
}
}
func makeRef(kind, name, sectionName string) structs.ResourceReference {
return structs.ResourceReference{
Kind: kind,
Name: name,
SectionName: sectionName,
}
}
func makeRoute(kind, name string, parents []structs.ResourceReference) structs.BoundRoute {
switch kind {
case structs.TCPRoute:
return &structs.TCPRouteConfigEntry{
Kind: structs.TCPRoute,
Name: name,
Parents: parents,
}
default:
panic("unknown route kind")
}
}
func makeListener(name string, routes []structs.ResourceReference) structs.BoundAPIGatewayListener {
return structs.BoundAPIGatewayListener{
Name: name,
Routes: routes,
}
}
func makeGateway(name string, listeners []structs.BoundAPIGatewayListener) *structs.BoundAPIGatewayConfigEntry {
return &structs.BoundAPIGatewayConfigEntry{
Kind: structs.BoundAPIGateway,
Name: name,
Listeners: listeners,
}
}

View File

@ -980,6 +980,77 @@ func (e *BoundAPIGatewayConfigEntry) GetEnterpriseMeta() *acl.EnterpriseMeta {
return &e.EnterpriseMeta
}
func (e *BoundAPIGatewayConfigEntry) UpdateRouteBinding(refs []ResourceReference, route BoundRoute) (bool, map[ResourceReference]error) {
didUpdate := false
errors := make(map[ResourceReference]error)
if len(e.Listeners) == 0 {
for _, ref := range refs {
errors[ref] = fmt.Errorf("route cannot bind because gateway has no listeners")
}
return false, errors
}
for i, listener := range e.Listeners {
// Unbind to handle any stale route references.
didUnbind := listener.UnbindRoute(route)
if didUnbind {
didUpdate = true
}
e.Listeners[i] = listener
for _, ref := range refs {
didBind, err := e.BindRoute(ref, route)
if err != nil {
errors[ref] = err
}
if didBind {
didUpdate = true
}
}
}
return didUpdate, errors
}
func (e *BoundAPIGatewayConfigEntry) BindRoute(ref ResourceReference, route BoundRoute) (bool, error) {
if ref.Kind != APIGateway || e.Name != ref.Name || !e.EnterpriseMeta.IsSame(&ref.EnterpriseMeta) {
return false, nil
}
if len(e.Listeners) == 0 {
return false, fmt.Errorf("route cannot bind because gateway has no listeners")
}
didBind := false
for i, listener := range e.Listeners {
if listener.Name == ref.SectionName || ref.SectionName == "" {
if listener.BindRoute(route) {
didBind = true
e.Listeners[i] = listener
}
}
}
if !didBind {
return false, fmt.Errorf("invalid section name: %s", ref.SectionName)
}
return true, nil
}
func (e *BoundAPIGatewayConfigEntry) UnbindRoute(route BoundRoute) bool {
didUnbind := false
for i, listener := range e.Listeners {
if listener.UnbindRoute(route) {
didUnbind = true
e.Listeners[i] = listener
}
}
return didUnbind
}
// BoundAPIGatewayListener is an API gateway listener with information
// about the routes and certificates that have successfully bound to it.
type BoundAPIGatewayListener struct {
@ -987,3 +1058,54 @@ type BoundAPIGatewayListener struct {
Routes []ResourceReference
Certificates []ResourceReference
}
// BindRoute is used to create or update a route on the listener.
// It returns true if the route was able to be bound to the listener.
func (l *BoundAPIGatewayListener) BindRoute(route BoundRoute) bool {
if l == nil {
return false
}
// TODO (t-eckert): Add a check that the listener has the same `protocol` as the route. Fail to bind if the protocols do not match.
// Convert the abstract route interface to a ResourceReference.
routeRef := ResourceReference{
Kind: route.GetKind(),
Name: route.GetName(),
EnterpriseMeta: *route.GetEnterpriseMeta(),
}
// If the listener has no routes, create a new slice of routes with the given route.
if l.Routes == nil {
l.Routes = []ResourceReference{routeRef}
return true
}
// If the route matches an existing route, update it and return.
for i, listenerRoute := range l.Routes {
if listenerRoute.Kind == routeRef.Kind && listenerRoute.Name == routeRef.Name && listenerRoute.EnterpriseMeta.IsSame(&routeRef.EnterpriseMeta) {
l.Routes[i] = routeRef
return true
}
}
// If the route is new to the listener, append it.
l.Routes = append(l.Routes, routeRef)
return true
}
func (l *BoundAPIGatewayListener) UnbindRoute(route BoundRoute) bool {
if l == nil {
return false
}
for i, listenerRoute := range l.Routes {
if listenerRoute.Kind == route.GetKind() && listenerRoute.Name == route.GetName() && listenerRoute.EnterpriseMeta.IsSame(route.GetEnterpriseMeta()) {
l.Routes = append(l.Routes[:i], l.Routes[i+1:]...)
return true
}
}
return false
}

View File

@ -1,6 +1,7 @@
package structs
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
@ -1341,3 +1342,456 @@ func TestBoundAPIGateway(t *testing.T) {
}
testConfigEntryNormalizeAndValidate(t, cases)
}
func TestBoundAPIGatewayBindRoute(t *testing.T) {
cases := map[string]struct {
gateway BoundAPIGatewayConfigEntry
route BoundRoute
expectedGateway BoundAPIGatewayConfigEntry
expectedDidBind bool
expectedErr error
}{
"Bind TCP Route to Gateway": {
gateway: BoundAPIGatewayConfigEntry{
Kind: BoundAPIGateway,
Name: "Test Bound API Gateway",
Listeners: []BoundAPIGatewayListener{
{
Name: "Test Listener",
Routes: []ResourceReference{},
},
},
},
route: &TCPRouteConfigEntry{
Kind: TCPRoute,
Name: "Test Route",
Parents: []ResourceReference{
{
Kind: APIGateway,
Name: "Test Bound API Gateway",
SectionName: "Test Listener",
},
},
},
expectedGateway: BoundAPIGatewayConfigEntry{
Kind: BoundAPIGateway,
Name: "Test Bound API Gateway",
Listeners: []BoundAPIGatewayListener{
{
Name: "Test Listener",
Routes: []ResourceReference{
{
Kind: TCPRoute,
Name: "Test Route",
},
},
},
},
},
expectedDidBind: true,
},
"Bind TCP Route with wildcard section name to all listeners on Gateway": {
gateway: BoundAPIGatewayConfigEntry{
Kind: BoundAPIGateway,
Name: "Test Bound API Gateway",
Listeners: []BoundAPIGatewayListener{
{
Name: "Test Listener 1",
Routes: []ResourceReference{},
},
{
Name: "Test Listener 2",
Routes: []ResourceReference{},
},
{
Name: "Test Listener 3",
Routes: []ResourceReference{},
},
},
},
route: &TCPRouteConfigEntry{
Kind: TCPRoute,
Name: "Test Route",
Parents: []ResourceReference{
{
Kind: APIGateway,
Name: "Test Bound API Gateway",
},
},
},
expectedGateway: BoundAPIGatewayConfigEntry{
Kind: BoundAPIGateway,
Name: "Test Bound API Gateway",
Listeners: []BoundAPIGatewayListener{
{
Name: "Test Listener 1",
Routes: []ResourceReference{
{
Kind: TCPRoute,
Name: "Test Route",
},
},
},
{
Name: "Test Listener 2",
Routes: []ResourceReference{
{
Kind: TCPRoute,
Name: "Test Route",
},
},
},
{
Name: "Test Listener 3",
Routes: []ResourceReference{
{
Kind: TCPRoute,
Name: "Test Route",
},
},
},
},
},
expectedDidBind: true,
},
"TCP Route cannot bind to Gateway because the parent reference kind is not APIGateway": {
gateway: BoundAPIGatewayConfigEntry{
Kind: BoundAPIGateway,
Name: "Test Bound API Gateway",
Listeners: []BoundAPIGatewayListener{},
},
route: &TCPRouteConfigEntry{
Kind: TCPRoute,
Name: "Test Route",
Parents: []ResourceReference{
{
Name: "Test Bound API Gateway",
SectionName: "Test Listener",
},
},
},
expectedGateway: BoundAPIGatewayConfigEntry{
Kind: TerminatingGateway,
Name: "Test Bound API Gateway",
Listeners: []BoundAPIGatewayListener{},
},
expectedDidBind: false,
expectedErr: nil,
},
"TCP Route cannot bind to Gateway because the parent reference name does not match": {
gateway: BoundAPIGatewayConfigEntry{
Kind: BoundAPIGateway,
Name: "Test Bound API Gateway",
Listeners: []BoundAPIGatewayListener{},
},
route: &TCPRouteConfigEntry{
Kind: TCPRoute,
Name: "Test Route",
Parents: []ResourceReference{
{
Kind: APIGateway,
Name: "Other Test Bound API Gateway",
SectionName: "Test Listener",
},
},
},
expectedGateway: BoundAPIGatewayConfigEntry{
Kind: BoundAPIGateway,
Name: "Test Bound API Gateway",
Listeners: []BoundAPIGatewayListener{},
},
expectedDidBind: false,
expectedErr: nil,
},
"TCP Route cannot bind to Gateway because it lacks listeners": {
gateway: BoundAPIGatewayConfigEntry{
Kind: BoundAPIGateway,
Name: "Test Bound API Gateway",
Listeners: []BoundAPIGatewayListener{},
},
route: &TCPRouteConfigEntry{
Kind: TCPRoute,
Name: "Test Route",
Parents: []ResourceReference{
{
Kind: APIGateway,
Name: "Test Bound API Gateway",
SectionName: "Test Listener",
},
},
},
expectedGateway: BoundAPIGatewayConfigEntry{
Kind: BoundAPIGateway,
Name: "Test Bound API Gateway",
Listeners: []BoundAPIGatewayListener{},
},
expectedDidBind: false,
expectedErr: fmt.Errorf("route cannot bind because gateway has no listeners"),
},
"TCP Route cannot bind to Gateway because it has an invalid section name": {
gateway: BoundAPIGatewayConfigEntry{
Kind: BoundAPIGateway,
Name: "Test Bound API Gateway",
Listeners: []BoundAPIGatewayListener{},
},
route: &TCPRouteConfigEntry{
Kind: TCPRoute,
Name: "Test Route",
Parents: []ResourceReference{
{
Kind: APIGateway,
Name: "Test Bound API Gateway",
SectionName: "Other Test Listener",
},
},
},
expectedGateway: BoundAPIGatewayConfigEntry{
Kind: BoundAPIGateway,
Name: "Test Bound API Gateway",
Listeners: []BoundAPIGatewayListener{},
},
expectedDidBind: false,
expectedErr: fmt.Errorf("route cannot bind because gateway has no listeners"),
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
ref := tc.route.GetParents()[0]
actualDidBind, actualErr := tc.gateway.BindRoute(ref, tc.route)
require.Equal(t, tc.expectedDidBind, actualDidBind)
require.Equal(t, tc.expectedErr, actualErr)
require.Equal(t, tc.expectedGateway.Listeners, tc.gateway.Listeners)
})
}
}
func TestBoundAPIGatewayUnbindRoute(t *testing.T) {
cases := map[string]struct {
gateway BoundAPIGatewayConfigEntry
route BoundRoute
expectedGateway BoundAPIGatewayConfigEntry
expectedDidUnbind bool
}{
"TCP Route unbinds from Gateway": {
gateway: BoundAPIGatewayConfigEntry{
Kind: BoundAPIGateway,
Name: "Test Bound API Gateway",
Listeners: []BoundAPIGatewayListener{
{
Name: "Test Listener",
Routes: []ResourceReference{
{
Kind: TCPRoute,
Name: "Test Route",
},
},
},
},
},
route: &TCPRouteConfigEntry{
Kind: TCPRoute,
Name: "Test Route",
Parents: []ResourceReference{
{
Kind: BoundAPIGateway,
Name: "Other Test Bound API Gateway",
SectionName: "Test Listener",
},
},
},
expectedGateway: BoundAPIGatewayConfigEntry{
Kind: BoundAPIGateway,
Name: "Test Bound API Gateway",
Listeners: []BoundAPIGatewayListener{
{
Name: "Test Listener",
Routes: []ResourceReference{},
},
},
},
expectedDidUnbind: true,
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
actualDidUnbind := tc.gateway.UnbindRoute(tc.route)
require.Equal(t, tc.expectedDidUnbind, actualDidUnbind)
require.Equal(t, tc.expectedGateway.Listeners, tc.gateway.Listeners)
})
}
}
func TestListenerBindRoute(t *testing.T) {
cases := map[string]struct {
listener BoundAPIGatewayListener
route BoundRoute
expectedListener BoundAPIGatewayListener
expectedDidBind bool
}{
"Listener has no routes": {
listener: BoundAPIGatewayListener{},
route: &TCPRouteConfigEntry{
Kind: TCPRoute,
Name: "Test Route",
},
expectedListener: BoundAPIGatewayListener{
Routes: []ResourceReference{
{
Kind: TCPRoute,
Name: "Test Route",
},
},
},
expectedDidBind: true,
},
"Listener to update existing route": {
listener: BoundAPIGatewayListener{
Routes: []ResourceReference{
{
Kind: TCPRoute,
Name: "Test Route 1",
},
{
Kind: TCPRoute,
Name: "Test Route 2",
},
{
Kind: TCPRoute,
Name: "Test Route 3",
},
},
},
route: &TCPRouteConfigEntry{
Kind: TCPRoute,
Name: "Test Route 2",
},
expectedListener: BoundAPIGatewayListener{
Routes: []ResourceReference{
{
Kind: TCPRoute,
Name: "Test Route 1",
},
{
Kind: TCPRoute,
Name: "Test Route 2",
},
{
Kind: TCPRoute,
Name: "Test Route 3",
},
},
},
expectedDidBind: true,
},
"Listener appends new route": {
listener: BoundAPIGatewayListener{
Routes: []ResourceReference{
{
Kind: TCPRoute,
Name: "Test Route 1",
},
{
Kind: TCPRoute,
Name: "Test Route 2",
},
},
},
route: &TCPRouteConfigEntry{
Kind: TCPRoute,
Name: "Test Route 3",
},
expectedListener: BoundAPIGatewayListener{
Routes: []ResourceReference{
{
Kind: TCPRoute,
Name: "Test Route 1",
},
{
Kind: TCPRoute,
Name: "Test Route 2",
},
{
Kind: TCPRoute,
Name: "Test Route 3",
},
},
},
expectedDidBind: true,
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
actualDidBind := tc.listener.BindRoute(tc.route)
require.Equal(t, tc.expectedDidBind, actualDidBind)
require.Equal(t, tc.expectedListener.Routes, tc.listener.Routes)
})
}
}
func TestListenerUnbindRoute(t *testing.T) {
cases := map[string]struct {
listener BoundAPIGatewayListener
route BoundRoute
expectedListener BoundAPIGatewayListener
expectedDidUnbind bool
}{
"Listener has no routes": {
listener: BoundAPIGatewayListener{},
route: &TCPRouteConfigEntry{
Kind: TCPRoute,
Name: "Test Route",
},
expectedListener: BoundAPIGatewayListener{},
expectedDidUnbind: false,
},
"Listener to remove existing route": {
listener: BoundAPIGatewayListener{
Routes: []ResourceReference{
{
Kind: TCPRoute,
Name: "Test Route 1",
},
{
Kind: TCPRoute,
Name: "Test Route 2",
},
{
Kind: TCPRoute,
Name: "Test Route 3",
},
},
},
route: &TCPRouteConfigEntry{
Kind: TCPRoute,
Name: "Test Route 2",
},
expectedListener: BoundAPIGatewayListener{
Routes: []ResourceReference{
{
Kind: TCPRoute,
Name: "Test Route 1",
},
{
Kind: TCPRoute,
Name: "Test Route 3",
},
},
},
expectedDidUnbind: true,
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
actualDidUnbind := tc.listener.UnbindRoute(tc.route)
require.Equal(t, tc.expectedDidUnbind, actualDidUnbind)
require.Equal(t, tc.expectedListener.Routes, tc.listener.Routes)
})
}
}

View File

@ -6,6 +6,13 @@ import (
"github.com/hashicorp/consul/acl"
)
// BoundRoute indicates a route that has parent gateways which
// can be accessed by calling the GetParents associated function.
type BoundRoute interface {
ConfigEntry
GetParents() []ResourceReference
}
// HTTPRouteConfigEntry manages the configuration for a HTTP route
// with the given name.
type HTTPRouteConfigEntry struct {
@ -85,6 +92,7 @@ type TCPRouteConfigEntry struct {
// Parents is a list of gateways that this route should be bound to
Parents []ResourceReference
// Services is a list of TCP-based services that this should route to.
// Currently, this must specify at maximum one service.
Services []TCPService
@ -152,6 +160,13 @@ func (e *TCPRouteConfigEntry) CanWrite(authz acl.Authorizer) error {
return authz.ToAllowAuthorizer().MeshWriteAllowed(&authzContext)
}
func (e *TCPRouteConfigEntry) GetParents() []ResourceReference {
if e == nil {
return []ResourceReference{}
}
return e.Parents
}
func (e *TCPRouteConfigEntry) GetRaftIndex() *RaftIndex {
if e == nil {
return &RaftIndex{}