Clean-up Gateway Controller Binding Logic (#16214)

* Fix detecting when a route doesn't bind to a gateway because it's already bound

* Clean up status setting code

* rework binding a bit

* More cleanup

* Flatten all files

* Fix up docstrings
This commit is contained in:
Andrew Stucki 2023-02-09 10:17:25 -05:00 committed by GitHub
parent d72ad5fb95
commit 0891b4554d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 2203 additions and 2441 deletions

View File

@ -1,152 +0,0 @@
package gateways
import (
"errors"
"time"
"github.com/hashicorp/consul/agent/configentry"
"github.com/hashicorp/consul/agent/consul/controller"
"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 []*gatewayMeta, routes ...structs.BoundRoute) ([]*structs.BoundAPIGatewayConfigEntry, []structs.ResourceReference, map[structs.ResourceReference]error) {
boundRefs := []structs.ResourceReference{}
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)
routeRef := structs.ResourceReference{
Kind: route.GetKind(),
Name: route.GetName(),
EnterpriseMeta: *route.GetEnterpriseMeta(),
}
// 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.BoundGateway)]
if routeReferencesGateway {
didUpdate, errors := gateway.updateRouteBinding(references, route)
if didUpdate {
modified = append(modified, gateway.BoundGateway)
}
for ref, err := range errors {
errored[ref] = err
}
for _, ref := range references {
delete(parentRefs, ref)
// this ref successfully bound, add it to the set that we'll update the
// status for
if _, found := errored[ref]; !found {
boundRefs = append(boundRefs, references...)
}
}
continue
}
if gateway.unbindRoute(routeRef) {
modified = append(modified, gateway.BoundGateway)
}
}
// 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, boundRefs, 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, pointerTo(ref.EnterpriseMeta))
gatewayRefs[kindName] = append(gatewayRefs[kindName], ref)
}
return parentRefs, gatewayRefs
}
func requestToResourceRef(req controller.Request) structs.ResourceReference {
ref := structs.ResourceReference{
Kind: req.Kind,
Name: req.Name,
}
if req.Meta != nil {
ref.EnterpriseMeta = *req.Meta
}
return ref
}
// RemoveGateway sets the route's status appropriately when the gateway that it's
// attempting to bind to does not exist
func RemoveGateway(gateway structs.ResourceReference, entries ...structs.BoundRoute) []structs.ControlledConfigEntry {
now := pointerTo(time.Now().UTC())
modified := []structs.ControlledConfigEntry{}
for _, route := range entries {
updater := structs.NewStatusUpdater(route)
for _, parent := range route.GetParents() {
if parent.Kind == gateway.Kind && parent.Name == gateway.Name && parent.EnterpriseMeta.IsSame(&gateway.EnterpriseMeta) {
updater.SetCondition(structs.Condition{
Type: "Bound",
Status: "False",
Reason: "GatewayNotFound",
Message: "gateway was not found",
Resource: pointerTo(parent),
LastTransitionTime: now,
})
}
}
if toUpdate, shouldUpdate := updater.UpdateEntry(); shouldUpdate {
modified = append(modified, toUpdate)
}
}
return modified
}
// RemoveRoute unbinds the route from the given gateways, returning the list of gateways that were modified.
func RemoveRoute(route structs.ResourceReference, entries ...*gatewayMeta) []*gatewayMeta {
modified := []*gatewayMeta{}
for _, entry := range entries {
if entry.unbindRoute(route) {
modified = append(modified, entry)
}
}
return modified
}

View File

@ -1,900 +0,0 @@
package gateways
import (
"fmt"
"testing"
"github.com/hashicorp/consul/agent/structs"
"github.com/stretchr/testify/require"
)
func TestBindRoutesToGateways(t *testing.T) {
t.Parallel()
type testCase struct {
gateways []*gatewayMeta
routes []structs.BoundRoute
expectedBoundAPIGateways []*structs.BoundAPIGatewayConfigEntry
expectedReferenceErrors map[structs.ResourceReference]error
}
cases := map[string]testCase{
"TCP Route binds to gateway": {
gateways: []*gatewayMeta{
{
BoundGateway: &structs.BoundAPIGatewayConfigEntry{
Name: "Gateway",
Listeners: []structs.BoundAPIGatewayListener{
{
Name: "Listener",
},
},
},
Gateway: &structs.APIGatewayConfigEntry{
Name: "Gateway",
Listeners: []structs.APIGatewayListener{
{
Name: "Listener",
Protocol: structs.ListenerProtocolTCP,
},
},
},
},
},
routes: []structs.BoundRoute{
&structs.TCPRouteConfigEntry{
Kind: structs.TCPRoute,
Name: "TCP Route",
Parents: []structs.ResourceReference{
{
Name: "Gateway",
Kind: structs.APIGateway,
SectionName: "Listener",
},
},
},
},
expectedBoundAPIGateways: []*structs.BoundAPIGatewayConfigEntry{
{
Name: "Gateway",
Listeners: []structs.BoundAPIGatewayListener{
{
Name: "Listener",
Routes: []structs.ResourceReference{
{
Name: "TCP Route",
Kind: structs.TCPRoute,
SectionName: "",
},
},
},
},
},
},
expectedReferenceErrors: map[structs.ResourceReference]error{},
},
"TCP Route unbinds from gateway": {
gateways: []*gatewayMeta{
{
BoundGateway: &structs.BoundAPIGatewayConfigEntry{
Name: "Gateway",
Listeners: []structs.BoundAPIGatewayListener{
{
Name: "Listener",
Routes: []structs.ResourceReference{
{
Name: "TCP Route",
Kind: structs.TCPRoute,
SectionName: "",
},
},
},
},
},
Gateway: &structs.APIGatewayConfigEntry{
Name: "Gateway",
Listeners: []structs.APIGatewayListener{
{
Name: "Listener",
Protocol: structs.ListenerProtocolTCP,
},
},
},
},
},
routes: []structs.BoundRoute{
&structs.TCPRouteConfigEntry{
Kind: structs.TCPRoute,
Name: "TCP Route",
Parents: []structs.ResourceReference{},
},
},
expectedBoundAPIGateways: []*structs.BoundAPIGatewayConfigEntry{
{
Name: "Gateway",
Listeners: []structs.BoundAPIGatewayListener{
{
Name: "Listener",
Routes: []structs.ResourceReference{},
},
},
},
},
expectedReferenceErrors: map[structs.ResourceReference]error{},
},
"TCP Route binds to multiple gateways": {
gateways: []*gatewayMeta{
{
BoundGateway: &structs.BoundAPIGatewayConfigEntry{
Name: "Gateway 1",
Listeners: []structs.BoundAPIGatewayListener{
{
Name: "Listener",
Routes: []structs.ResourceReference{},
},
},
},
Gateway: &structs.APIGatewayConfigEntry{
Name: "Gateway 1",
Listeners: []structs.APIGatewayListener{
{
Name: "Listener",
Protocol: structs.ListenerProtocolTCP,
},
},
},
},
{
BoundGateway: &structs.BoundAPIGatewayConfigEntry{
Name: "Gateway 2",
Listeners: []structs.BoundAPIGatewayListener{
{
Name: "Listener",
Routes: []structs.ResourceReference{},
},
},
},
Gateway: &structs.APIGatewayConfigEntry{
Name: "Gateway 2",
Listeners: []structs.APIGatewayListener{
{
Name: "Listener",
Protocol: structs.ListenerProtocolTCP,
},
},
},
},
},
routes: []structs.BoundRoute{
&structs.TCPRouteConfigEntry{
Kind: structs.TCPRoute,
Name: "TCP Route",
Parents: []structs.ResourceReference{
{
Name: "Gateway 1",
Kind: structs.APIGateway,
SectionName: "Listener",
},
{
Name: "Gateway 2",
Kind: structs.APIGateway,
SectionName: "Listener",
},
},
},
},
expectedBoundAPIGateways: []*structs.BoundAPIGatewayConfigEntry{
{
Name: "Gateway 1",
Listeners: []structs.BoundAPIGatewayListener{
{
Name: "Listener",
Routes: []structs.ResourceReference{
{
Name: "TCP Route",
Kind: structs.TCPRoute,
SectionName: "",
},
},
},
},
},
{
Name: "Gateway 2",
Listeners: []structs.BoundAPIGatewayListener{
{
Name: "Listener",
Routes: []structs.ResourceReference{
{
Name: "TCP Route",
Kind: structs.TCPRoute,
SectionName: "",
},
},
},
},
},
},
expectedReferenceErrors: map[structs.ResourceReference]error{},
},
"TCP Route binds to a single listener on a gateway with multiple listeners": {
gateways: []*gatewayMeta{
{
BoundGateway: &structs.BoundAPIGatewayConfigEntry{
Name: "Gateway",
Listeners: []structs.BoundAPIGatewayListener{
{
Name: "Listener 1",
Routes: []structs.ResourceReference{},
},
{
Name: "Listener 2",
Routes: []structs.ResourceReference{},
},
},
},
Gateway: &structs.APIGatewayConfigEntry{
Name: "Gateway",
Listeners: []structs.APIGatewayListener{
{
Name: "Listener 1",
Protocol: structs.ListenerProtocolHTTP,
},
{
Name: "Listener 2",
Protocol: structs.ListenerProtocolTCP,
},
},
},
},
},
routes: []structs.BoundRoute{
&structs.TCPRouteConfigEntry{
Name: "TCP Route",
Kind: structs.TCPRoute,
Parents: []structs.ResourceReference{
{
Name: "Gateway",
Kind: structs.APIGateway,
SectionName: "Listener 2",
},
},
},
},
expectedBoundAPIGateways: []*structs.BoundAPIGatewayConfigEntry{
{
Name: "Gateway",
Listeners: []structs.BoundAPIGatewayListener{
{
Name: "Listener 1",
Routes: []structs.ResourceReference{},
},
{
Name: "Listener 2",
Routes: []structs.ResourceReference{
{
Name: "TCP Route",
Kind: structs.TCPRoute,
SectionName: "",
},
},
},
},
},
},
expectedReferenceErrors: map[structs.ResourceReference]error{},
},
"TCP Route binds to all listeners on a gateway": {
gateways: []*gatewayMeta{
{
BoundGateway: &structs.BoundAPIGatewayConfigEntry{
Name: "Gateway",
Listeners: []structs.BoundAPIGatewayListener{
{
Name: "Listener 1",
Routes: []structs.ResourceReference{},
},
{
Name: "Listener 2",
Routes: []structs.ResourceReference{},
},
},
},
Gateway: &structs.APIGatewayConfigEntry{
Name: "Gateway",
Listeners: []structs.APIGatewayListener{
{
Name: "Listener 1",
Protocol: structs.ListenerProtocolTCP,
},
{
Name: "Listener 2",
Protocol: structs.ListenerProtocolTCP,
},
},
},
},
},
routes: []structs.BoundRoute{
&structs.TCPRouteConfigEntry{
Name: "TCP Route",
Kind: structs.TCPRoute,
Parents: []structs.ResourceReference{
{
Name: "Gateway",
Kind: structs.APIGateway,
SectionName: "",
},
},
},
},
expectedBoundAPIGateways: []*structs.BoundAPIGatewayConfigEntry{
{
Name: "Gateway",
Listeners: []structs.BoundAPIGatewayListener{
{
Name: "Listener 1",
Routes: []structs.ResourceReference{
{
Name: "TCP Route",
Kind: structs.TCPRoute,
SectionName: "",
},
},
},
{
Name: "Listener 2",
Routes: []structs.ResourceReference{
{
Name: "TCP Route",
Kind: structs.TCPRoute,
SectionName: "",
},
},
},
},
},
},
expectedReferenceErrors: map[structs.ResourceReference]error{},
},
"TCP Route binds to gateway with multiple listeners, one of which is already bound": {
gateways: []*gatewayMeta{
{
BoundGateway: &structs.BoundAPIGatewayConfigEntry{
Name: "Gateway",
Listeners: []structs.BoundAPIGatewayListener{
{
Name: "Listener 1",
Routes: []structs.ResourceReference{
{
Name: "TCP Route",
Kind: structs.TCPRoute,
SectionName: "",
},
},
},
{
Name: "Listener 2",
Routes: []structs.ResourceReference{},
},
},
},
Gateway: &structs.APIGatewayConfigEntry{
Name: "Gateway",
Listeners: []structs.APIGatewayListener{
{
Name: "Listener 1",
Protocol: structs.ListenerProtocolTCP,
},
{
Name: "Listener 2",
Protocol: structs.ListenerProtocolTCP,
},
},
},
},
},
routes: []structs.BoundRoute{
&structs.TCPRouteConfigEntry{
Name: "TCP Route",
Kind: structs.TCPRoute,
Parents: []structs.ResourceReference{
{
Name: "Gateway",
Kind: structs.APIGateway,
SectionName: "",
},
},
},
},
expectedBoundAPIGateways: []*structs.BoundAPIGatewayConfigEntry{
{
Name: "Gateway",
Listeners: []structs.BoundAPIGatewayListener{
{
Name: "Listener 1",
Routes: []structs.ResourceReference{
{
Name: "TCP Route",
Kind: structs.TCPRoute,
SectionName: "",
},
},
},
{
Name: "Listener 2",
Routes: []structs.ResourceReference{
{
Name: "TCP Route",
Kind: structs.TCPRoute,
SectionName: "",
},
},
},
},
},
},
expectedReferenceErrors: map[structs.ResourceReference]error{},
},
"TCP Route binds to a listener on multiple gateways": {
gateways: []*gatewayMeta{
{
BoundGateway: &structs.BoundAPIGatewayConfigEntry{
Name: "Gateway 1",
Listeners: []structs.BoundAPIGatewayListener{
{
Name: "Listener 1",
Routes: []structs.ResourceReference{},
},
{
Name: "Listener 2",
Routes: []structs.ResourceReference{},
},
},
},
Gateway: &structs.APIGatewayConfigEntry{
Name: "Gateway 1",
Listeners: []structs.APIGatewayListener{
{
Name: "Listener 1",
Protocol: structs.ListenerProtocolTCP,
},
{
Name: "Listener 2",
Protocol: structs.ListenerProtocolTCP,
},
},
},
},
{
BoundGateway: &structs.BoundAPIGatewayConfigEntry{
Name: "Gateway 2",
Listeners: []structs.BoundAPIGatewayListener{
{
Name: "Listener 1",
Routes: []structs.ResourceReference{},
},
{
Name: "Listener 2",
Routes: []structs.ResourceReference{},
},
},
},
Gateway: &structs.APIGatewayConfigEntry{
Name: "Gateway 2",
Listeners: []structs.APIGatewayListener{
{
Name: "Listener 1",
Protocol: structs.ListenerProtocolTCP,
},
{
Name: "Listener 2",
Protocol: structs.ListenerProtocolTCP,
},
},
},
},
},
routes: []structs.BoundRoute{
&structs.TCPRouteConfigEntry{
Name: "TCP Route",
Kind: structs.TCPRoute,
Parents: []structs.ResourceReference{
{
Name: "Gateway 1",
Kind: structs.APIGateway,
SectionName: "Listener 2",
},
{
Name: "Gateway 2",
Kind: structs.APIGateway,
SectionName: "Listener 2",
},
},
},
},
expectedBoundAPIGateways: []*structs.BoundAPIGatewayConfigEntry{
{
Name: "Gateway 1",
Listeners: []structs.BoundAPIGatewayListener{
{
Name: "Listener 1",
Routes: []structs.ResourceReference{},
},
{
Name: "Listener 2",
Routes: []structs.ResourceReference{
{
Name: "TCP Route",
Kind: structs.TCPRoute,
SectionName: "",
},
},
},
},
},
{
Name: "Gateway 2",
Listeners: []structs.BoundAPIGatewayListener{
{
Name: "Listener 1",
Routes: []structs.ResourceReference{},
},
{
Name: "Listener 2",
Routes: []structs.ResourceReference{
{
Name: "TCP Route",
Kind: structs.TCPRoute,
SectionName: "",
},
},
},
},
},
},
expectedReferenceErrors: map[structs.ResourceReference]error{},
},
"TCP Route swaps from one listener to another on a gateway": {
gateways: []*gatewayMeta{
{
BoundGateway: &structs.BoundAPIGatewayConfigEntry{
Name: "Gateway",
Listeners: []structs.BoundAPIGatewayListener{
{
Name: "Listener 1",
Routes: []structs.ResourceReference{
{
Name: "TCP Route",
Kind: structs.TCPRoute,
SectionName: "",
},
},
},
{
Name: "Listener 2",
Routes: []structs.ResourceReference{},
},
},
},
Gateway: &structs.APIGatewayConfigEntry{
Name: "Gateway",
Listeners: []structs.APIGatewayListener{
{
Name: "Listener 1",
Protocol: structs.ListenerProtocolTCP,
},
{
Name: "Listener 2",
Protocol: structs.ListenerProtocolTCP,
},
},
},
},
},
routes: []structs.BoundRoute{
&structs.TCPRouteConfigEntry{
Name: "TCP Route",
Kind: structs.TCPRoute,
Parents: []structs.ResourceReference{
{
Name: "Gateway",
Kind: structs.APIGateway,
SectionName: "Listener 2",
},
},
},
},
expectedBoundAPIGateways: []*structs.BoundAPIGatewayConfigEntry{
{
Name: "Gateway",
Listeners: []structs.BoundAPIGatewayListener{
{
Name: "Listener 1",
Routes: []structs.ResourceReference{},
},
{
Name: "Listener 2",
Routes: []structs.ResourceReference{
{
Name: "TCP Route",
Kind: structs.TCPRoute,
SectionName: "",
},
},
},
},
},
},
expectedReferenceErrors: map[structs.ResourceReference]error{},
},
"Multiple TCP Routes bind to different gateways": {
gateways: []*gatewayMeta{
{
BoundGateway: &structs.BoundAPIGatewayConfigEntry{
Name: "Gateway 1",
Listeners: []structs.BoundAPIGatewayListener{
{
Name: "Listener 1",
Routes: []structs.ResourceReference{},
},
},
},
Gateway: &structs.APIGatewayConfigEntry{
Name: "Gateway 1",
Listeners: []structs.APIGatewayListener{
{
Name: "Listener 1",
Protocol: structs.ListenerProtocolTCP,
},
},
},
},
{
BoundGateway: &structs.BoundAPIGatewayConfigEntry{
Name: "Gateway 2",
Listeners: []structs.BoundAPIGatewayListener{
{
Name: "Listener 2",
Routes: []structs.ResourceReference{},
},
},
},
Gateway: &structs.APIGatewayConfigEntry{
Name: "Gateway 2",
Listeners: []structs.APIGatewayListener{
{
Name: "Listener 2",
Protocol: structs.ListenerProtocolTCP,
},
},
},
},
},
routes: []structs.BoundRoute{
&structs.TCPRouteConfigEntry{
Name: "TCP Route 1",
Kind: structs.TCPRoute,
Parents: []structs.ResourceReference{
{
Name: "Gateway 1",
Kind: structs.APIGateway,
SectionName: "Listener 1",
},
},
},
&structs.TCPRouteConfigEntry{
Name: "TCP Route 2",
Kind: structs.TCPRoute,
Parents: []structs.ResourceReference{
{
Name: "Gateway 2",
Kind: structs.APIGateway,
SectionName: "Listener 2",
},
},
},
},
expectedBoundAPIGateways: []*structs.BoundAPIGatewayConfigEntry{
{
Name: "Gateway 1",
Listeners: []structs.BoundAPIGatewayListener{
{
Name: "Listener 1",
Routes: []structs.ResourceReference{
{
Name: "TCP Route 1",
Kind: structs.TCPRoute,
SectionName: "",
},
},
},
},
},
{
Name: "Gateway 2",
Listeners: []structs.BoundAPIGatewayListener{
{
Name: "Listener 2",
Routes: []structs.ResourceReference{
{
Name: "TCP Route 2",
Kind: structs.TCPRoute,
SectionName: "",
},
},
},
},
},
},
expectedReferenceErrors: map[structs.ResourceReference]error{},
},
"TCP Route cannot be bound to a listener with an HTTP protocol": {
gateways: []*gatewayMeta{
{
BoundGateway: &structs.BoundAPIGatewayConfigEntry{
Name: "Gateway",
Listeners: []structs.BoundAPIGatewayListener{
{
Name: "Listener",
Routes: []structs.ResourceReference{},
},
},
},
Gateway: &structs.APIGatewayConfigEntry{
Name: "Gateway",
Listeners: []structs.APIGatewayListener{
{
Name: "Listener",
Protocol: structs.ListenerProtocolHTTP,
},
},
},
},
},
routes: []structs.BoundRoute{
&structs.TCPRouteConfigEntry{
Name: "TCP Route",
Kind: structs.TCPRoute,
Parents: []structs.ResourceReference{
{
Name: "Gateway",
Kind: structs.APIGateway,
SectionName: "Listener",
},
},
},
},
expectedBoundAPIGateways: []*structs.BoundAPIGatewayConfigEntry{},
expectedReferenceErrors: map[structs.ResourceReference]error{
{
Name: "Gateway",
Kind: structs.APIGateway,
SectionName: "Listener",
}: fmt.Errorf("failed to bind route TCP Route to gateway Gateway: listener Listener is not a tcp listener"),
},
},
"If a route/listener protocol mismatch occurs with the wildcard, but a bind to another listener was possible, no error is returned": {
gateways: []*gatewayMeta{
{
BoundGateway: &structs.BoundAPIGatewayConfigEntry{
Name: "Gateway",
Listeners: []structs.BoundAPIGatewayListener{
{
Name: "Listener 1",
Routes: []structs.ResourceReference{},
},
{
Name: "Listener 2",
Routes: []structs.ResourceReference{},
},
},
},
Gateway: &structs.APIGatewayConfigEntry{
Name: "Gateway",
Listeners: []structs.APIGatewayListener{
{
Name: "Listener 1",
Protocol: structs.ListenerProtocolHTTP,
},
{
Name: "Listener 2",
Protocol: structs.ListenerProtocolTCP,
},
},
},
},
},
routes: []structs.BoundRoute{
&structs.TCPRouteConfigEntry{
Name: "TCP Route",
Kind: structs.TCPRoute,
Parents: []structs.ResourceReference{
{
Name: "Gateway",
Kind: structs.APIGateway,
SectionName: "",
},
},
},
},
expectedBoundAPIGateways: []*structs.BoundAPIGatewayConfigEntry{
{
Name: "Gateway",
Listeners: []structs.BoundAPIGatewayListener{
{
Name: "Listener 1",
Routes: []structs.ResourceReference{},
},
{
Name: "Listener 2",
Routes: []structs.ResourceReference{
{
Name: "TCP Route",
Kind: structs.TCPRoute,
SectionName: "",
},
},
},
},
},
},
expectedReferenceErrors: map[structs.ResourceReference]error{},
},
"TCP Route references a listener that does not exist": {
gateways: []*gatewayMeta{
{
BoundGateway: &structs.BoundAPIGatewayConfigEntry{
Name: "Gateway",
Listeners: []structs.BoundAPIGatewayListener{
{
Name: "Listener",
Routes: []structs.ResourceReference{},
},
},
},
Gateway: &structs.APIGatewayConfigEntry{
Name: "Gateway",
Listeners: []structs.APIGatewayListener{
{
Name: "Listener",
Protocol: structs.ListenerProtocolTCP,
},
},
},
},
},
routes: []structs.BoundRoute{
&structs.TCPRouteConfigEntry{
Name: "TCP Route",
Kind: structs.TCPRoute,
Parents: []structs.ResourceReference{
{
Name: "Gateway",
Kind: structs.APIGateway,
SectionName: "Non-existent Listener",
},
},
},
},
expectedBoundAPIGateways: []*structs.BoundAPIGatewayConfigEntry{},
expectedReferenceErrors: map[structs.ResourceReference]error{
{
Name: "Gateway",
Kind: structs.APIGateway,
SectionName: "Non-existent Listener",
}: fmt.Errorf("failed to bind route TCP Route to gateway Gateway: no valid listener has name 'Non-existent Listener' and uses tcp protocol"),
},
},
}
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)
})
}
}

View File

@ -2,6 +2,8 @@ package gateways
import (
"context"
"errors"
"fmt"
"sync"
"time"
@ -17,12 +19,21 @@ import (
"github.com/hashicorp/consul/agent/structs"
)
var (
errServiceDoesNotExist = errors.New("service does not exist")
errInvalidProtocol = errors.New("route protocol does not match targeted service protocol")
)
// Updater is a thin wrapper around a set of callbacks used for updating
// and deleting config entries via raft operations.
type Updater struct {
UpdateWithStatus func(entry structs.ControlledConfigEntry) error
Update func(entry structs.ConfigEntry) error
Delete func(entry structs.ConfigEntry) error
}
// apiGatewayReconciler is the monolithic reconciler used for reconciling
// all of our routes and gateways into bound gateway state.
type apiGatewayReconciler struct {
fsm *fsm.FSM
logger hclog.Logger
@ -30,6 +41,9 @@ type apiGatewayReconciler struct {
controller controller.Controller
}
// Reconcile is the main reconciliation function for the gateway reconciler, it
// delegates each reconciliation request to functions designated for a
// particular type of config entry.
func (r *apiGatewayReconciler) Reconcile(ctx context.Context, req controller.Request) error {
// We do this in a single threaded way to avoid race conditions around setting
// shared state. In our current out-of-repo code, this is handled via a global
@ -51,6 +65,9 @@ func (r *apiGatewayReconciler) Reconcile(ctx context.Context, req controller.Req
}
}
// reconcileEntry converts the controller request into a config entry that we then pass
// along to either a cleanup function if the entry no longer exists (it's been deleted),
// or a reconciler if the entry has been updated or created.
func reconcileEntry[T structs.ControlledConfigEntry](store *state.Store, logger hclog.Logger, ctx context.Context, req controller.Request, reconciler func(ctx context.Context, req controller.Request, store *state.Store, entry T) error, cleaner func(ctx context.Context, req controller.Request, store *state.Store) error) error {
_, entry, err := store.ConfigEntry(nil, req.Kind, req.Name, req.Meta)
if err != nil {
@ -122,7 +139,7 @@ func (r *apiGatewayReconciler) cleanupBoundGateway(_ context.Context, req contro
resource := requestToResourceRef(req)
resource.Kind = structs.APIGateway
for _, modifiedRoute := range RemoveGateway(resource, routes...) {
for _, modifiedRoute := range removeGateway(resource, routes...) {
routeLogger := routeLogger(logger, modifiedRoute)
routeLogger.Debug("persisting route status")
if err := r.updater.Update(modifiedRoute); err != nil {
@ -160,6 +177,9 @@ func (r *apiGatewayReconciler) reconcileBoundGateway(_ context.Context, req cont
return nil
}
// cleanupGateway deletes the associated bound gateway state with the config entry, route
// cleanup occurs when the bound gateway is re-reconciled or on the next reconciliation
// pass for the route.
func (r *apiGatewayReconciler) cleanupGateway(_ context.Context, req controller.Request, store *state.Store) error {
logger := gatewayRequestLogger(r.logger, req)
@ -181,8 +201,14 @@ func (r *apiGatewayReconciler) cleanupGateway(_ context.Context, req controller.
return nil
}
// reconcileGateway attempts to initialize or fetch the associated bound
// gateway state, fetch all route references, validate the existence of any
// referenced certificates, and then update the bound gateway with certificate
// references and add or remove any routes that reference or previously
// referenced this gateway. It then persists any status updates for the gateway,
// the modified routes, and updates the bound gateway.
func (r *apiGatewayReconciler) reconcileGateway(_ context.Context, req controller.Request, store *state.Store, gateway *structs.APIGatewayConfigEntry) error {
now := pointerTo(time.Now().UTC())
conditions := newGatewayConditionGenerator()
logger := gatewayRequestLogger(r.logger, req)
@ -206,8 +232,7 @@ func (r *apiGatewayReconciler) reconcileGateway(_ context.Context, req controlle
logger.Error("error retrieving bound api gateway", "error", err)
return err
}
meta := ensureInitializedMeta(gateway, bound)
meta := newGatewayMeta(gateway, bound)
certificateErrors, err := meta.checkCertificates(store)
if err != nil {
@ -216,73 +241,37 @@ func (r *apiGatewayReconciler) reconcileGateway(_ context.Context, req controlle
}
for ref, err := range certificateErrors {
updater.SetCondition(structs.Condition{
Type: "Accepted",
Status: "False",
Reason: "InvalidCertificate",
Message: err.Error(),
Resource: pointerTo(ref),
LastTransitionTime: now,
})
updater.SetCondition(conditions.invalidCertificate(ref, err))
}
if len(certificateErrors) > 0 {
updater.SetCondition(structs.Condition{
Type: "Accepted",
Status: "False",
Reason: "InvalidCertificates",
Message: "gateway references invalid certificates",
LastTransitionTime: now,
})
updater.SetCondition(conditions.invalidCertificates())
} else {
updater.SetCondition(structs.Condition{
Type: "Accepted",
Status: "True",
Reason: "Accepted",
Message: "gateway is valid",
LastTransitionTime: now,
})
updater.SetCondition(conditions.gatewayAccepted())
}
// now we bind all of the routes we can
updatedRoutes := []structs.ControlledConfigEntry{}
for _, route := range routes {
routeUpdater := structs.NewStatusUpdater(route)
_, boundRefs, bindErrors := BindRoutesToGateways([]*gatewayMeta{meta}, route)
_, boundRefs, bindErrors := bindRoutesToGateways(route, meta)
// unset the old gateway binding in case it's stale
for _, parent := range route.GetParents() {
if parent.Kind == gateway.Kind && parent.Name == gateway.Name && parent.EnterpriseMeta.IsSame(&gateway.EnterpriseMeta) {
routeUpdater.RemoveCondition(structs.Condition{
Type: "Bound",
Resource: pointerTo(parent),
})
routeUpdater.RemoveCondition(conditions.routeBound(parent))
}
}
// set the status for parents that have bound successfully
for _, ref := range boundRefs {
routeUpdater.SetCondition(structs.Condition{
Type: "Bound",
Status: "True",
Reason: "Bound",
Resource: pointerTo(ref),
Message: "successfully bound route",
LastTransitionTime: now,
})
routeUpdater.SetCondition(conditions.routeBound(ref))
}
// set the status for any parents that have errored trying to
// bind
for ref, err := range bindErrors {
routeUpdater.SetCondition(structs.Condition{
Type: "Bound",
Status: "False",
Reason: "FailedToBind",
Resource: pointerTo(ref),
Message: err.Error(),
LastTransitionTime: now,
})
routeUpdater.SetCondition(conditions.routeUnbound(ref, err))
}
// if we've updated any statuses, then store them as needing
@ -292,47 +281,8 @@ func (r *apiGatewayReconciler) reconcileGateway(_ context.Context, req controlle
}
}
// first check for gateway conflicts
for i, listener := range meta.BoundGateway.Listeners {
// TODO: refactor this to leverage something like checkConflicts
// that will require the ability to do something like pass in
// an updater since it's currently scoped to the function itself
protocol := meta.Gateway.Listeners[i].Protocol
switch protocol {
case structs.ListenerProtocolTCP:
if len(listener.Routes) > 1 {
updater.SetCondition(structs.Condition{
Type: "Conflicted",
Status: "True",
Reason: "RouteConflict",
Message: "TCP-based listeners currently only support binding a single route",
Resource: &structs.ResourceReference{
Kind: structs.APIGateway,
Name: meta.Gateway.Name,
SectionName: listener.Name,
EnterpriseMeta: meta.Gateway.EnterpriseMeta,
},
LastTransitionTime: now,
})
continue
}
}
updater.SetCondition(structs.Condition{
Type: "Conflicted",
Status: "False",
Reason: "NoConflict",
Resource: &structs.ResourceReference{
Kind: structs.APIGateway,
Name: meta.Gateway.Name,
SectionName: listener.Name,
EnterpriseMeta: meta.Gateway.EnterpriseMeta,
},
Message: "listener has no route conflicts",
LastTransitionTime: now,
})
}
// first set any gateway conflict statuses
meta.setConflicts(updater)
// now check if we need to update the gateway status
if modifiedGateway, shouldUpdate := updater.UpdateEntry(); shouldUpdate {
@ -354,7 +304,7 @@ func (r *apiGatewayReconciler) reconcileGateway(_ context.Context, req controlle
}
// now update the bound state if it changed
if bound == nil || stateIsDirty(bound.(*structs.BoundAPIGatewayConfigEntry), meta.BoundGateway) {
if bound == nil || !bound.(*structs.BoundAPIGatewayConfigEntry).IsSame(meta.BoundGateway) {
logger.Debug("persisting bound api gateway")
if err := r.updater.Update(meta.BoundGateway); err != nil {
logger.Error("error persisting bound api gateway", "error", err)
@ -365,6 +315,8 @@ func (r *apiGatewayReconciler) reconcileGateway(_ context.Context, req controlle
return nil
}
// cleanupRoute fetches all gateways and removes any existing reference to
// the route we're reconciling from them.
func (r *apiGatewayReconciler) cleanupRoute(_ context.Context, req controller.Request, store *state.Store) error {
logger := routeRequestLogger(r.logger, req)
@ -377,7 +329,7 @@ func (r *apiGatewayReconciler) cleanupRoute(_ context.Context, req controller.Re
return err
}
for _, modifiedGateway := range RemoveRoute(requestToResourceRef(req), meta...) {
for _, modifiedGateway := range removeRoute(requestToResourceRef(req), meta...) {
gatewayLogger := gatewayLogger(logger, modifiedGateway.BoundGateway)
gatewayLogger.Debug("persisting bound gateway state")
if err := r.updater.Update(modifiedGateway.BoundGateway); err != nil {
@ -391,9 +343,15 @@ func (r *apiGatewayReconciler) cleanupRoute(_ context.Context, req controller.Re
return nil
}
// Reconcile reconciles Route config entries.
// reconcileRoute attempts to validate a route against its referenced service
// discovery chain, it also fetches all gateways, and attempts to either remove
// the route being reconciled from gateways containing either stale references
// when this route no longer references them, or add the route to gateways that
// it now references. It then updates any necessary route statuses, checks for
// gateways that now have route conflicts, and updates all statuses and states
// as necessary.
func (r *apiGatewayReconciler) reconcileRoute(_ context.Context, req controller.Request, store *state.Store, route structs.BoundRoute) error {
now := pointerTo(time.Now().UTC())
conditions := newGatewayConditionGenerator()
logger := routeRequestLogger(r.logger, req)
@ -465,13 +423,7 @@ func (r *apiGatewayReconciler) reconcileRoute(_ context.Context, req controller.
})
if chainSet.IsEmpty() {
updater.SetCondition(structs.Condition{
Type: "Accepted",
Status: "False",
Reason: "InvalidDiscoveryChain",
Message: "service does not exist",
LastTransitionTime: now,
})
updater.SetCondition(conditions.routeInvalidDiscoveryChain(errServiceDoesNotExist))
continue
}
@ -490,13 +442,7 @@ func (r *apiGatewayReconciler) reconcileRoute(_ context.Context, req controller.
// discovery chain, but we still want to set watches on everything in the
// store
if validTargets {
updater.SetCondition(structs.Condition{
Type: "Accepted",
Status: "False",
Reason: "InvalidDiscoveryChain",
Message: err.Error(),
LastTransitionTime: now,
})
updater.SetCondition(conditions.routeInvalidDiscoveryChain(err))
validTargets = false
}
continue
@ -504,13 +450,7 @@ func (r *apiGatewayReconciler) reconcileRoute(_ context.Context, req controller.
if chain.Protocol != string(route.GetProtocol()) {
if validTargets {
updater.SetCondition(structs.Condition{
Type: "Accepted",
Status: "False",
Reason: "InvalidDiscoveryChain",
Message: "route protocol does not match targeted service protocol",
LastTransitionTime: now,
})
updater.SetCondition(conditions.routeInvalidDiscoveryChain(errInvalidProtocol))
validTargets = false
}
continue
@ -518,13 +458,7 @@ func (r *apiGatewayReconciler) reconcileRoute(_ context.Context, req controller.
// this makes sure we don't override an already set status
if validTargets {
updater.SetCondition(structs.Condition{
Type: "Accepted",
Status: "True",
Reason: "Accepted",
Message: "route is valid",
LastTransitionTime: now,
})
updater.SetCondition(conditions.routeAccepted())
}
}
@ -532,13 +466,7 @@ func (r *apiGatewayReconciler) reconcileRoute(_ context.Context, req controller.
// this should already happen in the validation check on write, but
// we'll do it here too just in case
if len(route.GetServiceNames()) == 0 {
updater.SetCondition(structs.Condition{
Type: "Accepted",
Status: "False",
Reason: "NoUpstreamServicesTargeted",
Message: "route must target at least one upstream service",
LastTransitionTime: now,
})
updater.SetCondition(conditions.routeNoUpstreams())
validTargets = false
}
@ -546,7 +474,7 @@ func (r *apiGatewayReconciler) reconcileRoute(_ context.Context, req controller.
// we return early, but need to make sure we're removed from all referencing
// gateways and our status is updated properly
updated := []*structs.BoundAPIGatewayConfigEntry{}
for _, modifiedGateway := range RemoveRoute(requestToResourceRef(req), meta...) {
for _, modifiedGateway := range removeRoute(requestToResourceRef(req), meta...) {
updated = append(updated, modifiedGateway.BoundGateway)
}
return finalize(updated)
@ -554,43 +482,46 @@ func (r *apiGatewayReconciler) reconcileRoute(_ context.Context, req controller.
// the route is valid, attempt to bind it to all gateways
r.logger.Debug("binding routes to gateway")
modifiedGateways, boundRefs, bindErrors := BindRoutesToGateways(meta, route)
modifiedGateways, boundRefs, bindErrors := bindRoutesToGateways(route, meta...)
// set the status of the references that are bound
for _, ref := range boundRefs {
updater.SetCondition(structs.Condition{
Type: "Bound",
Status: "True",
Reason: "Bound",
Resource: pointerTo(ref),
Message: "successfully bound route",
LastTransitionTime: now,
})
updater.SetCondition(conditions.routeBound(ref))
}
// set any binding errors
for ref, err := range bindErrors {
updater.SetCondition(structs.Condition{
Type: "Bound",
Status: "False",
Reason: "FailedToBind",
Resource: pointerTo(ref),
Message: err.Error(),
LastTransitionTime: now,
})
updater.SetCondition(conditions.routeUnbound(ref, err))
}
// set any refs that haven't been bound or explicitly errored
PARENT_LOOP:
for _, ref := range route.GetParents() {
for _, boundRef := range boundRefs {
if ref.IsSame(&boundRef) {
continue PARENT_LOOP
}
}
if _, ok := bindErrors[ref]; ok {
continue PARENT_LOOP
}
updater.SetCondition(conditions.gatewayNotFound(ref))
}
return finalize(modifiedGateways)
}
// reconcileHTTPRoute is a thin wrapper around recnocileRoute for a HTTPRoutes
func (r *apiGatewayReconciler) reconcileHTTPRoute(ctx context.Context, req controller.Request, store *state.Store, route *structs.HTTPRouteConfigEntry) error {
return r.reconcileRoute(ctx, req, store, route)
}
// reconcileTCPRoute is a thin wrapper around recnocileRoute for a TCPRoutes
func (r *apiGatewayReconciler) reconcileTCPRoute(ctx context.Context, req controller.Request, store *state.Store, route *structs.TCPRouteConfigEntry) error {
return r.reconcileRoute(ctx, req, store, route)
}
// NewAPIGatewayController initializes a controller that reconciles all APIGateway objects
func NewAPIGatewayController(fsm *fsm.FSM, publisher state.EventPublisher, updater *Updater, logger hclog.Logger) controller.Controller {
reconciler := &apiGatewayReconciler{
fsm: fsm,
@ -625,6 +556,525 @@ func NewAPIGatewayController(fsm *fsm.FSM, publisher state.EventPublisher, updat
})
}
// gatewayMeta embeds both a BoundAPIGateway and its corresponding APIGateway.
// This is used for binding routes to a gateway, because the binding logic
// requires correlation between fields on a gateway and a route, while persisting
// the state onto the corresponding subfields of a BoundAPIGateway. For example,
// when binding we need to validate that a route's protocol (e.g. http)
// matches the protocol of the listener it wants to bind to.
type gatewayMeta struct {
// BoundGateway is the bound-api-gateway config entry for a given gateway.
BoundGateway *structs.BoundAPIGatewayConfigEntry
// Gateway is the api-gateway config entry for the gateway.
Gateway *structs.APIGatewayConfigEntry
// listeners is a map of gateway listeners by name for fast access
// the map values are pointers so that we can update them directly
// and have the changes propagate back to the container gateways.
listeners map[string]*structs.APIGatewayListener
// boundListeners is a map of gateway listeners by name for fast access
// the map values are pointers so that we can update them directly
// and have the changes propagate back to the container gateways.
boundListeners map[string]*structs.BoundAPIGatewayListener
}
// getAllGatewayMeta returns a pre-constructed list of all valid gateway and state
// tuples based on the state coming from the store. Any gateway that does not have
// a corresponding bound-api-gateway config entry will be filtered out.
func getAllGatewayMeta(store *state.Store) ([]*gatewayMeta, error) {
_, gateways, err := store.ConfigEntriesByKind(nil, structs.APIGateway, acl.WildcardEnterpriseMeta())
if err != nil {
return nil, err
}
_, boundGateways, err := store.ConfigEntriesByKind(nil, structs.BoundAPIGateway, acl.WildcardEnterpriseMeta())
if err != nil {
return nil, err
}
meta := make([]*gatewayMeta, 0, len(boundGateways))
for _, b := range boundGateways {
bound := b.(*structs.BoundAPIGatewayConfigEntry)
for _, g := range gateways {
gateway := g.(*structs.APIGatewayConfigEntry)
if bound.IsInitializedForGateway(gateway) {
meta = append(meta, (&gatewayMeta{
BoundGateway: bound,
Gateway: gateway,
}).initialize())
break
}
}
}
return meta, nil
}
// updateRouteBinding takes a BoundRoute and modifies the listeners on the
// BoundAPIGateway config entry in GatewayMeta to reflect the binding of the
// route to the gateway.
//
// The return values correspond to:
// 1. whether the underlying BoundAPIGateway was actually modified
// 2. what references from the BoundRoute actually bound to the Gateway successfully
// 3. any errors that occurred while attempting to bind a particular reference to the Gateway
func (g *gatewayMeta) updateRouteBinding(route structs.BoundRoute) (bool, []structs.ResourceReference, map[structs.ResourceReference]error) {
errors := make(map[structs.ResourceReference]error)
boundRefs := []structs.ResourceReference{}
listenerUnbound := make(map[string]bool, len(g.boundListeners))
listenerBound := make(map[string]bool, len(g.boundListeners))
routeRef := structs.ResourceReference{
Kind: route.GetKind(),
Name: route.GetName(),
EnterpriseMeta: *route.GetEnterpriseMeta(),
}
// first attempt to unbind all of the routes from the listeners in case they're
// stale
g.eachListener(func(listener *structs.APIGatewayListener, bound *structs.BoundAPIGatewayListener) error {
listenerUnbound[listener.Name] = bound.UnbindRoute(routeRef)
return nil
})
// now try and bind all of the route's current refs
for _, ref := range route.GetParents() {
if !g.shouldBindRoute(ref) {
continue
}
if len(g.boundListeners) == 0 {
errors[ref] = fmt.Errorf("route cannot bind because gateway has no listeners")
continue
}
// try to bind to all listeners
refDidBind := false
g.eachListener(func(listener *structs.APIGatewayListener, bound *structs.BoundAPIGatewayListener) error {
didBind, err := g.bindRoute(listener, bound, route, ref)
if err != nil {
errors[ref] = err
}
if didBind {
refDidBind = true
listenerBound[listener.Name] = true
}
return nil
})
// double check that the wildcard ref actually bound to something
if !refDidBind && errors[ref] == nil {
errors[ref] = fmt.Errorf("failed to bind route %s to gateway %s with listener '%s'", route.GetName(), g.Gateway.Name, ref.SectionName)
}
if refDidBind {
boundRefs = append(boundRefs, ref)
}
}
didUpdate := false
for name, didUnbind := range listenerUnbound {
didBind := listenerBound[name]
if didBind != didUnbind {
didUpdate = true
break
}
}
return didUpdate, boundRefs, errors
}
// 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)
}
// shouldBindRouteToListener returns whether a Route's parent reference should attempt
// to bind to the given listener because it is either explicitly named or the Route
// is attempting to wildcard bind to the listener.
func (g *gatewayMeta) shouldBindRouteToListener(l *structs.BoundAPIGatewayListener, ref structs.ResourceReference) bool {
return l.Name == ref.SectionName || ref.SectionName == ""
}
// bindRoute takes a particular listener that a Route is attempting to bind to with a given reference
// and returns whether the Route successfully bound to the listener or if it errored in the process.
func (g *gatewayMeta) bindRoute(listener *structs.APIGatewayListener, bound *structs.BoundAPIGatewayListener, route structs.BoundRoute, ref structs.ResourceReference) (bool, error) {
if !g.shouldBindRouteToListener(bound, ref) {
return false, nil
}
if listener.Protocol == route.GetProtocol() && bound.BindRoute(structs.ResourceReference{
Kind: route.GetKind(),
Name: route.GetName(),
EnterpriseMeta: *route.GetEnterpriseMeta(),
}) {
return true, nil
}
if ref.SectionName != "" {
return false, fmt.Errorf("failed to bind route %s to gateway %s: listener %s is not a %s listener", route.GetName(), g.Gateway.Name, bound.Name, route.GetProtocol())
}
return false, nil
}
// unbindRoute takes a route and unbinds it from all of the listeners on a gateway.
// It returns true if the route was unbound and false if it was not.
func (g *gatewayMeta) unbindRoute(route structs.ResourceReference) bool {
didUnbind := false
for _, listener := range g.boundListeners {
if listener.UnbindRoute(route) {
didUnbind = true
}
}
return didUnbind
}
// eachListener iterates over all of the listeners for our underlying Gateway, it takes
// a callback function that can return an error, if an error is returned it halts execution
// and immediately returns the error.
func (g *gatewayMeta) eachListener(fn func(listener *structs.APIGatewayListener, bound *structs.BoundAPIGatewayListener) error) error {
for name, listener := range g.listeners {
if err := fn(listener, g.boundListeners[name]); err != nil {
return err
}
}
return nil
}
// checkCertificates verifies that all certificates referenced by the listeners on the gateway
// exist and collects them onto the bound gateway
func (g *gatewayMeta) checkCertificates(store *state.Store) (map[structs.ResourceReference]error, error) {
certificateErrors := map[structs.ResourceReference]error{}
err := g.eachListener(func(listener *structs.APIGatewayListener, bound *structs.BoundAPIGatewayListener) error {
for _, ref := range listener.TLS.Certificates {
_, certificate, err := store.ConfigEntry(nil, ref.Kind, ref.Name, &ref.EnterpriseMeta)
if err != nil {
return err
}
if certificate == nil {
certificateErrors[ref] = errors.New("certificate not found")
} else {
bound.Certificates = append(bound.Certificates, ref)
}
}
return nil
})
if err != nil {
return nil, err
}
return certificateErrors, nil
}
// checkConflicts returns whether a gateway status needs to be updated with
// conflicting route statuses
func (g *gatewayMeta) checkConflicts() (structs.ControlledConfigEntry, bool) {
updater := structs.NewStatusUpdater(g.Gateway)
g.setConflicts(updater)
return updater.UpdateEntry()
}
// setConflicts ensures that no TCP listener has more than the one allowed route and
// assigns an appropriate status
func (g *gatewayMeta) setConflicts(updater *structs.StatusUpdater) {
conditions := newGatewayConditionGenerator()
g.eachListener(func(listener *structs.APIGatewayListener, bound *structs.BoundAPIGatewayListener) error {
ref := structs.ResourceReference{
Kind: structs.APIGateway,
Name: g.Gateway.Name,
SectionName: listener.Name,
EnterpriseMeta: g.Gateway.EnterpriseMeta,
}
switch listener.Protocol {
case structs.ListenerProtocolTCP:
if len(bound.Routes) > 1 {
updater.SetCondition(conditions.gatewayListenerConflicts(ref))
return nil
}
}
updater.SetCondition(conditions.gatewayListenerNoConflicts(ref))
return nil
})
}
// initialize sets up the listener maps that we use for quickly indexing the listeners in our binding logic
func (g *gatewayMeta) initialize() *gatewayMeta {
// set up the maps for fast access
g.boundListeners = make(map[string]*structs.BoundAPIGatewayListener, len(g.BoundGateway.Listeners))
for i, listener := range g.BoundGateway.Listeners {
g.boundListeners[listener.Name] = &g.BoundGateway.Listeners[i]
}
g.listeners = make(map[string]*structs.APIGatewayListener, len(g.Gateway.Listeners))
for i, listener := range g.Gateway.Listeners {
g.listeners[listener.Name] = &g.Gateway.Listeners[i]
}
return g
}
// newGatewayMeta returns an object that wraps the given APIGateway and BoundAPIGateway
func newGatewayMeta(gateway *structs.APIGatewayConfigEntry, bound structs.ConfigEntry) *gatewayMeta {
var b *structs.BoundAPIGatewayConfigEntry
if bound == nil {
b = &structs.BoundAPIGatewayConfigEntry{
Kind: structs.BoundAPIGateway,
Name: gateway.Name,
EnterpriseMeta: gateway.EnterpriseMeta,
}
} else {
b = bound.(*structs.BoundAPIGatewayConfigEntry).DeepCopy()
}
// we just clear out the bound state here since we recalculate it entirely
// in the gateway control loop
listeners := make([]structs.BoundAPIGatewayListener, 0, len(gateway.Listeners))
for _, listener := range gateway.Listeners {
listeners = append(listeners, structs.BoundAPIGatewayListener{
Name: listener.Name,
})
}
b.Listeners = listeners
return (&gatewayMeta{
BoundGateway: b,
Gateway: gateway,
}).initialize()
}
// gatewayConditionGenerator is a simple struct used for isolating
// the status conditions that we generate for our components
type gatewayConditionGenerator struct {
now *time.Time
}
// newGatewayConditionGenerator initializes a status conditions generator
func newGatewayConditionGenerator() *gatewayConditionGenerator {
return &gatewayConditionGenerator{
now: pointerTo(time.Now().UTC()),
}
}
// invalidCertificate returns a condition used when a gateway references a
// certificate that does not exist. It takes a ref used to scope the condition
// to a given APIGateway listener.
func (g *gatewayConditionGenerator) invalidCertificate(ref structs.ResourceReference, err error) structs.Condition {
return structs.Condition{
Type: "Accepted",
Status: "False",
Reason: "InvalidCertificate",
Message: err.Error(),
Resource: pointerTo(ref),
LastTransitionTime: g.now,
}
}
// invalidCertificates is used to set the overall condition of the APIGateway
// to invalid due to missing certificates that it references.
func (g *gatewayConditionGenerator) invalidCertificates() structs.Condition {
return structs.Condition{
Type: "Accepted",
Status: "False",
Reason: "InvalidCertificates",
Message: "gateway references invalid certificates",
LastTransitionTime: g.now,
}
}
// gatewayAccepted marks the APIGateway as valid.
func (g *gatewayConditionGenerator) gatewayAccepted() structs.Condition {
return structs.Condition{
Type: "Accepted",
Status: "True",
Reason: "Accepted",
Message: "gateway is valid",
LastTransitionTime: g.now,
}
}
// routeBound marks a Route as bound to the referenced APIGateway
func (g *gatewayConditionGenerator) routeBound(ref structs.ResourceReference) structs.Condition {
return structs.Condition{
Type: "Bound",
Status: "True",
Reason: "Bound",
Resource: pointerTo(ref),
Message: "successfully bound route",
LastTransitionTime: g.now,
}
}
// routeAccepted marks the Route as valid
func (g *gatewayConditionGenerator) routeAccepted() structs.Condition {
return structs.Condition{
Type: "Accepted",
Status: "True",
Reason: "Accepted",
Message: "route is valid",
LastTransitionTime: g.now,
}
}
// routeUnbound marks the route as having failed to bind to the referenced APIGateway
func (g *gatewayConditionGenerator) routeUnbound(ref structs.ResourceReference, err error) structs.Condition {
return structs.Condition{
Type: "Bound",
Status: "False",
Reason: "FailedToBind",
Resource: pointerTo(ref),
Message: err.Error(),
LastTransitionTime: g.now,
}
}
// routeInvalidDiscoveryChain marks the route as invalid due to an error while validating its referenced
// discovery chian
func (g *gatewayConditionGenerator) routeInvalidDiscoveryChain(err error) structs.Condition {
return structs.Condition{
Type: "Accepted",
Status: "False",
Reason: "InvalidDiscoveryChain",
Message: err.Error(),
LastTransitionTime: g.now,
}
}
// routeNoUpstreams marks the route as invalid because it has no upstreams that it targets
func (g *gatewayConditionGenerator) routeNoUpstreams() structs.Condition {
return structs.Condition{
Type: "Accepted",
Status: "False",
Reason: "NoUpstreamServicesTargeted",
Message: "route must target at least one upstream service",
LastTransitionTime: g.now,
}
}
// gatewayListenerConflicts marks an APIGateway listener as having bound routes that conflict with each other
// and make the listener, therefore invalid
func (g *gatewayConditionGenerator) gatewayListenerConflicts(ref structs.ResourceReference) structs.Condition {
return structs.Condition{
Type: "Conflicted",
Status: "True",
Reason: "RouteConflict",
Resource: pointerTo(ref),
Message: "TCP-based listeners currently only support binding a single route",
LastTransitionTime: g.now,
}
}
// gatewayListenerNoConflicts marks an APIGateway listener as having no conflicts within its
// bound routes
func (g *gatewayConditionGenerator) gatewayListenerNoConflicts(ref structs.ResourceReference) structs.Condition {
return structs.Condition{
Type: "Conflicted",
Status: "False",
Reason: "NoConflict",
Resource: pointerTo(ref),
Message: "listener has no route conflicts",
LastTransitionTime: g.now,
}
}
// gatewayNotFound marks a Route as having failed to bind to a referenced APIGateway due to
// the Gateway not existing (or having not been reconciled yet)
func (g *gatewayConditionGenerator) gatewayNotFound(ref structs.ResourceReference) structs.Condition {
return structs.Condition{
Type: "Bound",
Status: "False",
Reason: "GatewayNotFound",
Resource: pointerTo(ref),
Message: "gateway was not found",
LastTransitionTime: g.now,
}
}
// bindRoutesToGateways takes a route variadic number of gateways.
// It iterates over the parent references for the 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 list of parent references on the route that were successfully used to bind the route, and
// a map of resource references to errors that occurred when they were attempted to be
// bound to a gateway.
func bindRoutesToGateways(route structs.BoundRoute, gateways ...*gatewayMeta) ([]*structs.BoundAPIGatewayConfigEntry, []structs.ResourceReference, map[structs.ResourceReference]error) {
boundRefs := []structs.ResourceReference{}
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)
// Iterate over all BoundAPIGateway config entries and try to bind them to the route if they are a parent.
for _, gateway := range gateways {
didUpdate, bound, errors := gateway.updateRouteBinding(route)
if didUpdate {
modified = append(modified, gateway.BoundGateway)
}
for ref, err := range errors {
errored[ref] = err
}
boundRefs = append(boundRefs, bound...)
}
return modified, boundRefs, errored
}
// removeGateway sets the route's status appropriately when the gateway that it's
// attempting to bind to does not exist
func removeGateway(gateway structs.ResourceReference, entries ...structs.BoundRoute) []structs.ControlledConfigEntry {
conditions := newGatewayConditionGenerator()
modified := []structs.ControlledConfigEntry{}
for _, route := range entries {
updater := structs.NewStatusUpdater(route)
for _, parent := range route.GetParents() {
if parent.Kind == gateway.Kind && parent.Name == gateway.Name && parent.EnterpriseMeta.IsSame(&gateway.EnterpriseMeta) {
updater.SetCondition(conditions.gatewayNotFound(parent))
}
}
if toUpdate, shouldUpdate := updater.UpdateEntry(); shouldUpdate {
modified = append(modified, toUpdate)
}
}
return modified
}
// removeRoute unbinds the route from the given gateways, returning the list of gateways that were modified.
func removeRoute(route structs.ResourceReference, entries ...*gatewayMeta) []*gatewayMeta {
modified := []*gatewayMeta{}
for _, entry := range entries {
if entry.unbindRoute(route) {
modified = append(modified, entry)
}
}
return modified
}
// requestToResourceRef constructs a resource reference from the given controller request
func requestToResourceRef(req controller.Request) structs.ResourceReference {
ref := structs.ResourceReference{
Kind: req.Kind,
Name: req.Name,
}
if req.Meta != nil {
ref.EnterpriseMeta = *req.Meta
}
return ref
}
// retrieveAllRoutesFromStore retrieves all HTTP and TCP routes from the given store
func retrieveAllRoutesFromStore(store *state.Store) ([]structs.BoundRoute, error) {
_, httpRoutes, err := store.ConfigEntriesByKind(nil, structs.HTTPRoute, acl.WildcardEnterpriseMeta())
if err != nil {
@ -649,35 +1099,46 @@ func retrieveAllRoutesFromStore(store *state.Store) ([]structs.BoundRoute, error
return routes, nil
}
// pointerTo returns a pointer to the value passed as an argument
func pointerTo[T any](value T) *T {
return &value
}
// requestLogger returns a logger that adds some request-specific fields to the given logger
func requestLogger(logger hclog.Logger, request controller.Request) hclog.Logger {
meta := request.Meta
return logger.With("kind", request.Kind, "name", request.Name, "namespace", meta.NamespaceOrDefault(), "partition", meta.PartitionOrDefault())
}
// certificateRequestLogger returns a logger that adds some certificate-specific fields to the given logger
func certificateRequestLogger(logger hclog.Logger, request controller.Request) hclog.Logger {
meta := request.Meta
return logger.With("inline-certificate", request.Name, "namespace", meta.NamespaceOrDefault(), "partition", meta.PartitionOrDefault())
}
// gatewayRequestLogger returns a logger that adds some gateway-specific fields to the given logger
func gatewayRequestLogger(logger hclog.Logger, request controller.Request) hclog.Logger {
meta := request.Meta
return logger.With("gateway", request.Name, "namespace", meta.NamespaceOrDefault(), "partition", meta.PartitionOrDefault())
}
// gatewayLogger returns a logger that adds some gateway-specific fields to the given logger,
// it should be used when logging info about a gateway resource being modified from a non-gateway
// reconciliation funciton
func gatewayLogger(logger hclog.Logger, gateway structs.ConfigEntry) hclog.Logger {
meta := gateway.GetEnterpriseMeta()
return logger.With("gateway.name", gateway.GetName(), "gateway.namespace", meta.NamespaceOrDefault(), "gateway.partition", meta.PartitionOrDefault())
}
// routeRequestLogger returns a logger that adds some route-specific fields to the given logger
func routeRequestLogger(logger hclog.Logger, request controller.Request) hclog.Logger {
meta := request.Meta
return logger.With("kind", request.Kind, "route", request.Name, "namespace", meta.NamespaceOrDefault(), "partition", meta.PartitionOrDefault())
}
// routeLogger returns a logger that adds some route-specific fields to the given logger,
// it should be used when logging info about a route resource being modified from a non-route
// reconciliation funciton
func routeLogger(logger hclog.Logger, route structs.ConfigEntry) hclog.Logger {
meta := route.GetEnterpriseMeta()
return logger.With("route.kind", route.GetKind(), "route.name", route.GetName(), "route.namespace", meta.NamespaceOrDefault(), "route.partition", meta.PartitionOrDefault())

File diff suppressed because it is too large Load Diff

View File

@ -1,306 +0,0 @@
package gateways
import (
"errors"
"fmt"
"time"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/consul/state"
"github.com/hashicorp/consul/agent/structs"
)
// gatewayMeta embeds both a BoundAPIGateway and its corresponding APIGateway.
// This is used when binding routes to a gateway to ensure that a route's protocol (e.g. http)
// matches the protocol of the listener it wants to bind to. The binding modifies the
// "bound" gateway, but relies on the "gateway" to determine the protocol of the listener.
type gatewayMeta struct {
// BoundGateway is the bound-api-gateway config entry for a given gateway.
BoundGateway *structs.BoundAPIGatewayConfigEntry
// Gateway is the api-gateway config entry for the gateway.
Gateway *structs.APIGatewayConfigEntry
}
// getAllGatewayMeta returns a pre-constructed list of all valid gateway and state
// tuples based on the state coming from the store. Any gateway that does not have
// a corresponding bound-api-gateway config entry will be filtered out.
func getAllGatewayMeta(store *state.Store) ([]*gatewayMeta, error) {
_, gateways, err := store.ConfigEntriesByKind(nil, structs.APIGateway, acl.WildcardEnterpriseMeta())
if err != nil {
return nil, err
}
_, boundGateways, err := store.ConfigEntriesByKind(nil, structs.BoundAPIGateway, acl.WildcardEnterpriseMeta())
if err != nil {
return nil, err
}
meta := make([]*gatewayMeta, 0, len(boundGateways))
for _, b := range boundGateways {
bound := b.(*structs.BoundAPIGatewayConfigEntry)
for _, g := range gateways {
gateway := g.(*structs.APIGatewayConfigEntry)
if bound.IsInitializedForGateway(gateway) {
meta = append(meta, &gatewayMeta{
BoundGateway: bound,
Gateway: gateway,
})
break
}
}
}
return meta, nil
}
// updateRouteBinding takes a parent resource reference and a BoundRoute and
// modifies the listeners on the BoundAPIGateway config entry in GatewayMeta
// to reflect the binding of the route to the gateway.
//
// If the reference is not valid or the route's protocol does not match the
// targeted listener's protocol, a mapping of parent references to associated
// errors is returned.
func (g *gatewayMeta) updateRouteBinding(refs []structs.ResourceReference, route structs.BoundRoute) (bool, map[structs.ResourceReference]error) {
if g.BoundGateway == nil || g.Gateway == nil {
return false, nil
}
didUpdate := false
errors := make(map[structs.ResourceReference]error)
if len(g.BoundGateway.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 g.BoundGateway.Listeners {
routeRef := structs.ResourceReference{
Kind: route.GetKind(),
Name: route.GetName(),
EnterpriseMeta: *route.GetEnterpriseMeta(),
}
// Unbind to handle any stale route references.
didUnbind := listener.UnbindRoute(routeRef)
if didUnbind {
didUpdate = true
}
g.BoundGateway.Listeners[i] = listener
for _, ref := range refs {
didBind, err := g.bindRoute(ref, route)
if err != nil {
errors[ref] = err
}
if didBind {
didUpdate = true
}
}
}
return didUpdate, errors
}
// bindRoute takes a parent reference and a route and attempts to bind the route to the
// bound gateway in the gatewayMeta struct. It returns true if the route was bound and
// false if it was not. If the route fails to bind, an error is returned.
//
// Binding logic binds a route to one or more listeners on the Bound gateway.
// For a route to successfully bind it must:
// - have a parent reference to the gateway
// - have a parent reference with a section name matching the name of a listener
// on the gateway. If the section name is `""`, the route will be bound to all
// listeners on the gateway whose protocol matches the route's protocol.
// - have a protocol that matches the protocol of the listener it is being bound to.
func (g *gatewayMeta) bindRoute(ref structs.ResourceReference, route structs.BoundRoute) (bool, error) {
if g.BoundGateway == nil || g.Gateway == nil {
return false, fmt.Errorf("gateway cannot be found")
}
if ref.Kind != structs.APIGateway || g.Gateway.Name != ref.Name || !g.Gateway.EnterpriseMeta.IsSame(&ref.EnterpriseMeta) {
return false, nil
}
if len(g.BoundGateway.Listeners) == 0 {
return false, fmt.Errorf("route cannot bind because gateway has no listeners")
}
didBind := false
for _, listener := range g.Gateway.Listeners {
// A route with a section name of "" is bound to all listeners on the gateway.
if listener.Name != ref.SectionName && ref.SectionName != "" {
continue
}
if listener.Protocol == route.GetProtocol() {
routeRef := structs.ResourceReference{
Kind: route.GetKind(),
Name: route.GetName(),
EnterpriseMeta: *route.GetEnterpriseMeta(),
}
i, boundListener := g.boundListenerByName(listener.Name)
if boundListener != nil && boundListener.BindRoute(routeRef) {
didBind = true
g.BoundGateway.Listeners[i] = *boundListener
}
} else if ref.SectionName != "" {
// Failure to bind to a specific listener is an error
return false, fmt.Errorf("failed to bind route %s to gateway %s: listener %s is not a %s listener", route.GetName(), g.Gateway.Name, listener.Name, route.GetProtocol())
}
}
if !didBind {
return didBind, fmt.Errorf("failed to bind route %s to gateway %s: no valid listener has name '%s' and uses %s protocol", route.GetName(), g.Gateway.Name, ref.SectionName, route.GetProtocol())
}
return didBind, nil
}
// unbindRoute takes a route and unbinds it from all of the listeners on a gateway.
// It returns true if the route was unbound and false if it was not.
func (g *gatewayMeta) unbindRoute(route structs.ResourceReference) bool {
if g.BoundGateway == nil {
return false
}
didUnbind := false
for i, listener := range g.BoundGateway.Listeners {
if listener.UnbindRoute(route) {
didUnbind = true
g.BoundGateway.Listeners[i] = listener
}
}
return didUnbind
}
func (g *gatewayMeta) boundListenerByName(name string) (int, *structs.BoundAPIGatewayListener) {
for i, listener := range g.BoundGateway.Listeners {
if listener.Name == name {
return i, &listener
}
}
return -1, nil
}
// checkCertificates verifies that all certificates referenced by the listeners on the gateway
// exist and collects them onto the bound gateway
func (g *gatewayMeta) checkCertificates(store *state.Store) (map[structs.ResourceReference]error, error) {
certificateErrors := map[structs.ResourceReference]error{}
for i, listener := range g.Gateway.Listeners {
bound := g.BoundGateway.Listeners[i]
for _, ref := range listener.TLS.Certificates {
_, certificate, err := store.ConfigEntry(nil, ref.Kind, ref.Name, &ref.EnterpriseMeta)
if err != nil {
return nil, err
}
if certificate == nil {
certificateErrors[ref] = errors.New("certificate not found")
} else {
bound.Certificates = append(bound.Certificates, ref)
}
}
}
return certificateErrors, nil
}
// checkConflicts ensures that no TCP listener has more than the one allowed route and
// assigns an appropriate status
func (g *gatewayMeta) checkConflicts() (structs.ControlledConfigEntry, bool) {
now := pointerTo(time.Now().UTC())
updater := structs.NewStatusUpdater(g.Gateway)
for i, listener := range g.BoundGateway.Listeners {
protocol := g.Gateway.Listeners[i].Protocol
switch protocol {
case structs.ListenerProtocolTCP:
if len(listener.Routes) > 1 {
updater.SetCondition(structs.Condition{
Type: "Conflicted",
Status: "True",
Reason: "RouteConflict",
Resource: &structs.ResourceReference{
Kind: structs.APIGateway,
Name: g.Gateway.Name,
SectionName: listener.Name,
EnterpriseMeta: g.Gateway.EnterpriseMeta,
},
Message: "TCP-based listeners currently only support binding a single route",
LastTransitionTime: now,
})
}
continue
}
updater.SetCondition(structs.Condition{
Type: "Conflicted",
Status: "False",
Reason: "NoConflict",
Resource: &structs.ResourceReference{
Kind: structs.APIGateway,
Name: g.Gateway.Name,
SectionName: listener.Name,
EnterpriseMeta: g.Gateway.EnterpriseMeta,
},
Message: "listener has no route conflicts",
LastTransitionTime: now,
})
}
return updater.UpdateEntry()
}
func ensureInitializedMeta(gateway *structs.APIGatewayConfigEntry, bound structs.ConfigEntry) *gatewayMeta {
var b *structs.BoundAPIGatewayConfigEntry
if bound == nil {
b = &structs.BoundAPIGatewayConfigEntry{
Kind: structs.BoundAPIGateway,
Name: gateway.Name,
EnterpriseMeta: gateway.EnterpriseMeta,
}
} else {
b = bound.(*structs.BoundAPIGatewayConfigEntry).DeepCopy()
}
// we just clear out the bound state here since we recalculate it entirely
// in the gateway control loop
listeners := make([]structs.BoundAPIGatewayListener, 0, len(gateway.Listeners))
for _, listener := range gateway.Listeners {
listeners = append(listeners, structs.BoundAPIGatewayListener{
Name: listener.Name,
})
}
b.Listeners = listeners
return &gatewayMeta{
BoundGateway: b,
Gateway: gateway,
}
}
func stateIsDirty(initial, final *structs.BoundAPIGatewayConfigEntry) bool {
initialListeners := map[string]structs.BoundAPIGatewayListener{}
for _, listener := range initial.Listeners {
initialListeners[listener.Name] = listener
}
finalListeners := map[string]structs.BoundAPIGatewayListener{}
for _, listener := range final.Listeners {
finalListeners[listener.Name] = listener
}
if len(initialListeners) != len(finalListeners) {
return true
}
for name, initialListener := range initialListeners {
finalListener, found := finalListeners[name]
if !found {
return true
}
if !initialListener.IsSame(finalListener) {
return true
}
}
return false
}

View File

@ -1,378 +0,0 @@
package gateways
import (
"fmt"
"testing"
"github.com/hashicorp/consul/agent/structs"
"github.com/stretchr/testify/require"
)
func TestBoundAPIGatewayBindRoute(t *testing.T) {
t.Parallel()
cases := map[string]struct {
gateway gatewayMeta
route structs.BoundRoute
expectedBoundGateway structs.BoundAPIGatewayConfigEntry
expectedDidBind bool
expectedErr error
}{
"Bind TCP Route to Gateway": {
gateway: gatewayMeta{
BoundGateway: &structs.BoundAPIGatewayConfigEntry{
Kind: structs.BoundAPIGateway,
Name: "Gateway",
Listeners: []structs.BoundAPIGatewayListener{
{
Name: "Listener",
Routes: []structs.ResourceReference{},
},
},
},
Gateway: &structs.APIGatewayConfigEntry{
Kind: structs.APIGateway,
Name: "Gateway",
Listeners: []structs.APIGatewayListener{
{
Name: "Listener",
Protocol: structs.ListenerProtocolTCP,
},
},
},
},
route: &structs.TCPRouteConfigEntry{
Kind: structs.TCPRoute,
Name: "Route",
Parents: []structs.ResourceReference{
{
Kind: structs.APIGateway,
Name: "Gateway",
SectionName: "Listener",
},
},
},
expectedBoundGateway: structs.BoundAPIGatewayConfigEntry{
Kind: structs.BoundAPIGateway,
Name: "Gateway",
Listeners: []structs.BoundAPIGatewayListener{
{
Name: "Listener",
Routes: []structs.ResourceReference{
{
Kind: structs.TCPRoute,
Name: "Route",
},
},
},
},
},
expectedDidBind: true,
},
"Bind TCP Route with wildcard section name to all listeners on Gateway": {
gateway: gatewayMeta{
BoundGateway: &structs.BoundAPIGatewayConfigEntry{
Kind: structs.BoundAPIGateway,
Name: "Gateway",
Listeners: []structs.BoundAPIGatewayListener{
{
Name: "Listener 1",
Routes: []structs.ResourceReference{},
},
{
Name: "Listener 2",
Routes: []structs.ResourceReference{},
},
{
Name: "Listener 3",
Routes: []structs.ResourceReference{},
},
},
},
Gateway: &structs.APIGatewayConfigEntry{
Kind: structs.APIGateway,
Name: "Gateway",
Listeners: []structs.APIGatewayListener{
{
Name: "Listener 1",
Protocol: structs.ListenerProtocolTCP,
},
{
Name: "Listener 2",
Protocol: structs.ListenerProtocolTCP,
},
{
Name: "Listener 3",
Protocol: structs.ListenerProtocolTCP,
},
},
},
},
route: &structs.TCPRouteConfigEntry{
Kind: structs.TCPRoute,
Name: "Route",
Parents: []structs.ResourceReference{
{
Kind: structs.APIGateway,
Name: "Gateway",
},
},
},
expectedBoundGateway: structs.BoundAPIGatewayConfigEntry{
Kind: structs.BoundAPIGateway,
Name: "Gateway",
Listeners: []structs.BoundAPIGatewayListener{
{
Name: "Listener 1",
Routes: []structs.ResourceReference{
{
Kind: structs.TCPRoute,
Name: "Route",
},
},
},
{
Name: "Listener 2",
Routes: []structs.ResourceReference{
{
Kind: structs.TCPRoute,
Name: "Route",
},
},
},
{
Name: "Listener 3",
Routes: []structs.ResourceReference{
{
Kind: structs.TCPRoute,
Name: "Route",
},
},
},
},
},
expectedDidBind: true,
},
"TCP Route cannot bind to Gateway because the parent reference kind is not APIGateway": {
gateway: gatewayMeta{
BoundGateway: &structs.BoundAPIGatewayConfigEntry{
Kind: structs.BoundAPIGateway,
Name: "Gateway",
Listeners: []structs.BoundAPIGatewayListener{},
},
Gateway: &structs.APIGatewayConfigEntry{
Kind: structs.APIGateway,
Name: "Gateway",
Listeners: []structs.APIGatewayListener{},
},
},
route: &structs.TCPRouteConfigEntry{
Kind: structs.TCPRoute,
Name: "Route",
Parents: []structs.ResourceReference{
{
Name: "Gateway",
SectionName: "Listener",
},
},
},
expectedBoundGateway: structs.BoundAPIGatewayConfigEntry{
Kind: structs.TerminatingGateway,
Name: "Gateway",
Listeners: []structs.BoundAPIGatewayListener{},
},
expectedDidBind: false,
expectedErr: nil,
},
"TCP Route cannot bind to Gateway because the parent reference name does not match": {
gateway: gatewayMeta{
BoundGateway: &structs.BoundAPIGatewayConfigEntry{
Kind: structs.BoundAPIGateway,
Name: "Gateway",
Listeners: []structs.BoundAPIGatewayListener{},
},
Gateway: &structs.APIGatewayConfigEntry{
Kind: structs.APIGateway,
Name: "Gateway",
Listeners: []structs.APIGatewayListener{},
},
},
route: &structs.TCPRouteConfigEntry{
Kind: structs.TCPRoute,
Name: "Route",
Parents: []structs.ResourceReference{
{
Kind: structs.APIGateway,
Name: "Other Gateway",
SectionName: "Listener",
},
},
},
expectedBoundGateway: structs.BoundAPIGatewayConfigEntry{
Kind: structs.BoundAPIGateway,
Name: "Gateway",
Listeners: []structs.BoundAPIGatewayListener{},
},
expectedDidBind: false,
expectedErr: nil,
},
"TCP Route cannot bind to Gateway because it lacks listeners": {
gateway: gatewayMeta{
BoundGateway: &structs.BoundAPIGatewayConfigEntry{
Kind: structs.BoundAPIGateway,
Name: "Gateway",
Listeners: []structs.BoundAPIGatewayListener{},
},
Gateway: &structs.APIGatewayConfigEntry{
Kind: structs.APIGateway,
Name: "Gateway",
Listeners: []structs.APIGatewayListener{},
},
},
route: &structs.TCPRouteConfigEntry{
Kind: structs.TCPRoute,
Name: "Route",
Parents: []structs.ResourceReference{
{
Kind: structs.APIGateway,
Name: "Gateway",
SectionName: "Listener",
},
},
},
expectedBoundGateway: structs.BoundAPIGatewayConfigEntry{
Kind: structs.BoundAPIGateway,
Name: "Gateway",
Listeners: []structs.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: gatewayMeta{
BoundGateway: &structs.BoundAPIGatewayConfigEntry{
Kind: structs.BoundAPIGateway,
Name: "Gateway",
Listeners: []structs.BoundAPIGatewayListener{
{
Name: "Listener",
Routes: []structs.ResourceReference{},
},
},
},
Gateway: &structs.APIGatewayConfigEntry{
Kind: structs.APIGateway,
Name: "Gateway",
Listeners: []structs.APIGatewayListener{
{
Name: "Listener",
Protocol: structs.ListenerProtocolTCP,
},
},
},
},
route: &structs.TCPRouteConfigEntry{
Kind: structs.TCPRoute,
Name: "Route",
Parents: []structs.ResourceReference{
{
Kind: structs.APIGateway,
Name: "Gateway",
SectionName: "Other Listener",
},
},
},
expectedBoundGateway: structs.BoundAPIGatewayConfigEntry{
Kind: structs.BoundAPIGateway,
Name: "Gateway",
Listeners: []structs.BoundAPIGatewayListener{
{
Name: "Listener",
Routes: []structs.ResourceReference{},
},
},
},
expectedDidBind: false,
expectedErr: fmt.Errorf("failed to bind route Route to gateway Gateway: no valid listener has name 'Other Listener' and uses tcp protocol"),
},
}
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.expectedBoundGateway.Listeners, tc.gateway.BoundGateway.Listeners)
})
}
}
func TestBoundAPIGatewayUnbindRoute(t *testing.T) {
t.Parallel()
cases := map[string]struct {
gateway gatewayMeta
route structs.BoundRoute
expectedGateway structs.BoundAPIGatewayConfigEntry
expectedDidUnbind bool
}{
"TCP Route unbinds from Gateway": {
gateway: gatewayMeta{
BoundGateway: &structs.BoundAPIGatewayConfigEntry{
Kind: structs.BoundAPIGateway,
Name: "Gateway",
Listeners: []structs.BoundAPIGatewayListener{
{
Name: "Listener",
Routes: []structs.ResourceReference{
{
Kind: structs.TCPRoute,
Name: "Route",
},
},
},
},
},
},
route: &structs.TCPRouteConfigEntry{
Kind: structs.TCPRoute,
Name: "Route",
Parents: []structs.ResourceReference{
{
Kind: structs.BoundAPIGateway,
Name: "Gateway",
SectionName: "Listener",
},
},
},
expectedGateway: structs.BoundAPIGatewayConfigEntry{
Kind: structs.BoundAPIGateway,
Name: "Gateway",
Listeners: []structs.BoundAPIGatewayListener{
{
Name: "Listener",
Routes: []structs.ResourceReference{},
},
},
},
expectedDidUnbind: true,
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
routeRef := structs.ResourceReference{
Kind: tc.route.GetKind(),
Name: tc.route.GetName(),
EnterpriseMeta: *tc.route.GetEnterpriseMeta(),
}
actualDidUnbind := tc.gateway.unbindRoute(routeRef)
require.Equal(t, tc.expectedDidUnbind, actualDidUnbind)
require.Equal(t, tc.expectedGateway.Listeners, tc.gateway.BoundGateway.Listeners)
})
}
}

View File

@ -916,6 +916,34 @@ type BoundAPIGatewayConfigEntry struct {
RaftIndex
}
func (e *BoundAPIGatewayConfigEntry) IsSame(other *BoundAPIGatewayConfigEntry) bool {
listeners := map[string]BoundAPIGatewayListener{}
for _, listener := range e.Listeners {
listeners[listener.Name] = listener
}
otherListeners := map[string]BoundAPIGatewayListener{}
for _, listener := range other.Listeners {
otherListeners[listener.Name] = listener
}
if len(listeners) != len(otherListeners) {
return false
}
for name, listener := range listeners {
otherListener, found := otherListeners[name]
if !found {
return false
}
if !listener.IsSame(otherListener) {
return false
}
}
return true
}
// IsInitializedForGateway returns whether or not this bound api gateway is initialized with the given api gateway
// including having corresponding listener entries for the gateway.
func (e *BoundAPIGatewayConfigEntry) IsInitializedForGateway(gateway *APIGatewayConfigEntry) bool {
@ -1059,10 +1087,6 @@ func (l BoundAPIGatewayListener) IsSame(other BoundAPIGatewayListener) bool {
// and protocol. Be sure to check both of these before attempting
// to bind a route to the listener.
func (l *BoundAPIGatewayListener) BindRoute(routeRef ResourceReference) bool {
if l == nil {
return false
}
// If the listener has no routes, create a new slice of routes with the given route.
if l.Routes == nil {
l.Routes = []ResourceReference{routeRef}