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:
parent
ada3530213
commit
b01dca96af
|
@ -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
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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{}
|
||||
|
|
Loading…
Reference in New Issue