From 0891b4554d041c439f0864b5a6f60ad39c3c86fb Mon Sep 17 00:00:00 2001 From: Andrew Stucki Date: Thu, 9 Feb 2023 10:17:25 -0500 Subject: [PATCH] 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 --- agent/consul/gateways/bind.go | 152 -- agent/consul/gateways/bind_test.go | 900 ------- agent/consul/gateways/controller_gateways.go | 751 ++++-- .../gateways/controller_gateways_test.go | 2125 ++++++++++++----- agent/consul/gateways/gateway_meta.go | 306 --- agent/consul/gateways/gateway_meta_test.go | 378 --- agent/structs/config_entry_gateways.go | 32 +- 7 files changed, 2203 insertions(+), 2441 deletions(-) delete mode 100644 agent/consul/gateways/bind.go delete mode 100644 agent/consul/gateways/bind_test.go delete mode 100644 agent/consul/gateways/gateway_meta.go delete mode 100644 agent/consul/gateways/gateway_meta_test.go diff --git a/agent/consul/gateways/bind.go b/agent/consul/gateways/bind.go deleted file mode 100644 index 17763bf972..0000000000 --- a/agent/consul/gateways/bind.go +++ /dev/null @@ -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 -} diff --git a/agent/consul/gateways/bind_test.go b/agent/consul/gateways/bind_test.go deleted file mode 100644 index 42f67fa21f..0000000000 --- a/agent/consul/gateways/bind_test.go +++ /dev/null @@ -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) - }) - } -} diff --git a/agent/consul/gateways/controller_gateways.go b/agent/consul/gateways/controller_gateways.go index bf37eeaad2..40faa33224 100644 --- a/agent/consul/gateways/controller_gateways.go +++ b/agent/consul/gateways/controller_gateways.go @@ -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()) diff --git a/agent/consul/gateways/controller_gateways_test.go b/agent/consul/gateways/controller_gateways_test.go index b9a31f6c3e..aa7b747fd1 100644 --- a/agent/consul/gateways/controller_gateways_test.go +++ b/agent/consul/gateways/controller_gateways_test.go @@ -3,6 +3,7 @@ package gateways import ( "context" "encoding/json" + "errors" "fmt" "testing" "time" @@ -17,7 +18,1327 @@ import ( "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, + }, + "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, + }, + "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 with listener 'Other Listener'"), + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + ref := tc.route.GetParents()[0] + + actualDidBind, _, actualErrors := (&tc.gateway).initialize().updateRouteBinding(tc.route) + + require.Equal(t, tc.expectedDidBind, actualDidBind) + require.Equal(t, tc.expectedErr, actualErrors[ref]) + 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", + }, + }, + }, + }, + }, + Gateway: &structs.APIGatewayConfigEntry{}, + }, + 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).initialize().unbindRoute(routeRef) + + require.Equal(t, tc.expectedDidUnbind, actualDidUnbind) + require.Equal(t, tc.expectedGateway.Listeners, tc.gateway.BoundGateway.Listeners) + }) + } +} + +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 with listener 'Non-existent Listener'"), + }, + }, + "Already bound TCP Route": { + gateways: []*gatewayMeta{ + { + BoundGateway: &structs.BoundAPIGatewayConfigEntry{ + Name: "Gateway", + Listeners: []structs.BoundAPIGatewayListener{ + { + Name: "Listener", + Routes: []structs.ResourceReference{{ + Kind: structs.TCPRoute, + Name: "TCP Route", + }}, + }, + }, + }, + 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, + }, + }, + }, + }, + expectedBoundAPIGateways: []*structs.BoundAPIGatewayConfigEntry{}, + expectedReferenceErrors: map[structs.ResourceReference]error{}, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + for i := range tc.gateways { + tc.gateways[i].initialize() + } + + actualBoundAPIGatewaysMap := make(map[string]*structs.BoundAPIGatewayConfigEntry) + referenceErrors := make(map[structs.ResourceReference]error) + for _, route := range tc.routes { + bound, _, errs := bindRoutesToGateways(route, tc.gateways...) + for ref, err := range errs { + referenceErrors[ref] = err + } + for _, g := range bound { + actualBoundAPIGatewaysMap[g.Name] = g + } + } + + actualBoundAPIGateways := []*structs.BoundAPIGatewayConfigEntry{} + for _, g := range actualBoundAPIGatewaysMap { + actualBoundAPIGateways = append(actualBoundAPIGateways, g) + } + + require.ElementsMatch(t, tc.expectedBoundAPIGateways, actualBoundAPIGateways) + require.Equal(t, tc.expectedReferenceErrors, referenceErrors) + }) + } +} + func TestAPIGatewayController(t *testing.T) { + conditions := newGatewayConditionGenerator() defaultMeta := acl.DefaultEnterpriseMeta() for name, tc := range map[string]struct { requests []controller.Request @@ -44,12 +1365,9 @@ func TestAPIGatewayController(t *testing.T) { Name: "gateway", EnterpriseMeta: *defaultMeta, Status: structs.Status{ - Conditions: []structs.Condition{{ - Type: "Accepted", - Status: "True", - Reason: "Accepted", - Message: "gateway is valid", - }}, + Conditions: []structs.Condition{ + conditions.gatewayAccepted(), + }, }, }, &structs.BoundAPIGatewayConfigEntry{ @@ -78,12 +1396,9 @@ func TestAPIGatewayController(t *testing.T) { Name: "tcp-route", EnterpriseMeta: *defaultMeta, Status: structs.Status{ - Conditions: []structs.Condition{{ - Type: "Accepted", - Status: "False", - Reason: "NoUpstreamServicesTargeted", - Message: "route must target at least one upstream service", - }}, + Conditions: []structs.Condition{ + conditions.routeNoUpstreams(), + }, }, }, }, @@ -107,12 +1422,9 @@ func TestAPIGatewayController(t *testing.T) { Name: "http-route", EnterpriseMeta: *defaultMeta, Status: structs.Status{ - Conditions: []structs.Condition{{ - Type: "Accepted", - Status: "False", - Reason: "NoUpstreamServicesTargeted", - Message: "route must target at least one upstream service", - }}, + Conditions: []structs.Condition{ + conditions.routeNoUpstreams(), + }, }, }, }, @@ -139,12 +1451,9 @@ func TestAPIGatewayController(t *testing.T) { Name: "tcp-route", EnterpriseMeta: *defaultMeta, Status: structs.Status{ - Conditions: []structs.Condition{{ - Type: "Accepted", - Status: "False", - Reason: "InvalidDiscoveryChain", - Message: "service does not exist", - }}, + Conditions: []structs.Condition{ + conditions.routeInvalidDiscoveryChain(errServiceDoesNotExist), + }, }, }, }, @@ -177,12 +1486,9 @@ func TestAPIGatewayController(t *testing.T) { Name: "tcp-route", EnterpriseMeta: *defaultMeta, Status: structs.Status{ - Conditions: []structs.Condition{{ - Type: "Accepted", - Status: "False", - Reason: "InvalidDiscoveryChain", - Message: "route protocol does not match targeted service protocol", - }}, + Conditions: []structs.Condition{ + conditions.routeInvalidDiscoveryChain(errInvalidProtocol), + }, }, }, }, @@ -219,22 +1525,14 @@ func TestAPIGatewayController(t *testing.T) { Name: "tcp-route", EnterpriseMeta: *defaultMeta, Status: structs.Status{ - Conditions: []structs.Condition{{ - Type: "Accepted", - Status: "True", - Reason: "Accepted", - Message: "route is valid", - }, { - Type: "Bound", - Status: "False", - Reason: "FailedToBind", - Resource: &structs.ResourceReference{ + Conditions: []structs.Condition{ + conditions.routeAccepted(), + conditions.gatewayNotFound(structs.ResourceReference{ Kind: structs.APIGateway, Name: "gateway", EnterpriseMeta: *defaultMeta, - }, - Message: "invalid reference to missing parent", - }}, + }), + }, }, }, }, @@ -281,22 +1579,14 @@ func TestAPIGatewayController(t *testing.T) { Name: "tcp-route", EnterpriseMeta: *defaultMeta, Status: structs.Status{ - Conditions: []structs.Condition{{ - Type: "Accepted", - Status: "True", - Reason: "Accepted", - Message: "route is valid", - }, { - Type: "Bound", - Status: "False", - Reason: "FailedToBind", - Resource: &structs.ResourceReference{ + Conditions: []structs.Condition{ + conditions.routeAccepted(), + conditions.gatewayNotFound(structs.ResourceReference{ Kind: structs.APIGateway, Name: "gateway", EnterpriseMeta: *defaultMeta, - }, - Message: "invalid reference to missing parent", - }}, + }), + }, }, }, }, @@ -353,22 +1643,14 @@ func TestAPIGatewayController(t *testing.T) { Name: "tcp-route", EnterpriseMeta: *defaultMeta, Status: structs.Status{ - Conditions: []structs.Condition{{ - Type: "Accepted", - Status: "True", - Reason: "Accepted", - Message: "route is valid", - }, { - Type: "Bound", - Status: "False", - Reason: "FailedToBind", - Resource: &structs.ResourceReference{ + Conditions: []structs.Condition{ + conditions.routeAccepted(), + conditions.routeUnbound(structs.ResourceReference{ Kind: structs.APIGateway, Name: "gateway", EnterpriseMeta: *defaultMeta, - }, - Message: "route cannot bind because gateway has no listeners", - }}, + }, errors.New("route cannot bind because gateway has no listeners")), + }, }, }, }, @@ -430,22 +1712,14 @@ func TestAPIGatewayController(t *testing.T) { Name: "tcp-route", EnterpriseMeta: *defaultMeta, Status: structs.Status{ - Conditions: []structs.Condition{{ - Type: "Accepted", - Status: "True", - Reason: "Accepted", - Message: "route is valid", - }, { - Type: "Bound", - Status: "False", - Reason: "FailedToBind", - Resource: &structs.ResourceReference{ + Conditions: []structs.Condition{ + conditions.routeAccepted(), + conditions.gatewayNotFound(structs.ResourceReference{ Kind: structs.APIGateway, Name: "gateway", EnterpriseMeta: *defaultMeta, - }, - Message: "invalid reference to missing parent", - }}, + }), + }, }, }, }, @@ -514,23 +1788,15 @@ func TestAPIGatewayController(t *testing.T) { Name: "gateway", EnterpriseMeta: *defaultMeta, Status: structs.Status{ - Conditions: []structs.Condition{{ - Type: "Accepted", - Status: "True", - Reason: "Accepted", - Message: "gateway is valid", - }, { - Type: "Conflicted", - Status: "False", - Reason: "NoConflict", - Resource: &structs.ResourceReference{ + Conditions: []structs.Condition{ + conditions.gatewayAccepted(), + conditions.gatewayListenerNoConflicts(structs.ResourceReference{ Kind: structs.APIGateway, Name: "gateway", EnterpriseMeta: *defaultMeta, SectionName: "listener", - }, - Message: "listener has no route conflicts", - }}, + }), + }, }, }, &structs.TCPRouteConfigEntry{ @@ -538,22 +1804,14 @@ func TestAPIGatewayController(t *testing.T) { Name: "tcp-route", EnterpriseMeta: *defaultMeta, Status: structs.Status{ - Conditions: []structs.Condition{{ - Type: "Accepted", - Status: "True", - Reason: "Accepted", - Message: "route is valid", - }, { - Type: "Bound", - Status: "True", - Reason: "Bound", - Resource: &structs.ResourceReference{ + Conditions: []structs.Condition{ + conditions.routeAccepted(), + conditions.routeBound(structs.ResourceReference{ Kind: structs.APIGateway, Name: "gateway", EnterpriseMeta: *defaultMeta, - }, - Message: "successfully bound route", - }}, + }), + }, }, }, }, @@ -617,23 +1875,15 @@ func TestAPIGatewayController(t *testing.T) { Name: "gateway", EnterpriseMeta: *defaultMeta, Status: structs.Status{ - Conditions: []structs.Condition{{ - Type: "Accepted", - Status: "True", - Reason: "Accepted", - Message: "gateway is valid", - }, { - Type: "Conflicted", - Status: "False", - Reason: "NoConflict", - Resource: &structs.ResourceReference{ + Conditions: []structs.Condition{ + conditions.gatewayAccepted(), + conditions.gatewayListenerNoConflicts(structs.ResourceReference{ Kind: structs.APIGateway, Name: "gateway", EnterpriseMeta: *defaultMeta, SectionName: "listener", - }, - Message: "listener has no route conflicts", - }}, + }), + }, }, }, &structs.TCPRouteConfigEntry{ @@ -641,22 +1891,14 @@ func TestAPIGatewayController(t *testing.T) { Name: "tcp-route", EnterpriseMeta: *defaultMeta, Status: structs.Status{ - Conditions: []structs.Condition{{ - Type: "Accepted", - Status: "True", - Reason: "Accepted", - Message: "route is valid", - }, { - Type: "Bound", - Status: "False", - Reason: "FailedToBind", - Resource: &structs.ResourceReference{ + Conditions: []structs.Condition{ + conditions.routeAccepted(), + conditions.routeUnbound(structs.ResourceReference{ Kind: structs.APIGateway, Name: "gateway", EnterpriseMeta: *defaultMeta, - }, - Message: "failed to bind route tcp-route to gateway gateway: no valid listener has name '' and uses tcp protocol", - }}, + }, errors.New("failed to bind route tcp-route to gateway gateway with listener ''")), + }, }, }, }, @@ -746,23 +1988,15 @@ func TestAPIGatewayController(t *testing.T) { Name: "gateway", EnterpriseMeta: *defaultMeta, Status: structs.Status{ - Conditions: []structs.Condition{{ - Type: "Accepted", - Status: "True", - Reason: "Accepted", - Message: "gateway is valid", - }, { - Type: "Conflicted", - Status: "True", - Reason: "RouteConflict", - Resource: &structs.ResourceReference{ + Conditions: []structs.Condition{ + conditions.gatewayAccepted(), + conditions.gatewayListenerConflicts(structs.ResourceReference{ Kind: structs.APIGateway, Name: "gateway", EnterpriseMeta: *defaultMeta, SectionName: "listener", - }, - Message: "TCP-based listeners currently only support binding a single route", - }}, + }), + }, }, }, &structs.TCPRouteConfigEntry{ @@ -770,22 +2004,14 @@ func TestAPIGatewayController(t *testing.T) { Name: "tcp-route-one", EnterpriseMeta: *defaultMeta, Status: structs.Status{ - Conditions: []structs.Condition{{ - Type: "Accepted", - Status: "True", - Reason: "Accepted", - Message: "route is valid", - }, { - Type: "Bound", - Status: "True", - Reason: "Bound", - Resource: &structs.ResourceReference{ + Conditions: []structs.Condition{ + conditions.routeAccepted(), + conditions.routeBound(structs.ResourceReference{ Kind: structs.APIGateway, Name: "gateway", EnterpriseMeta: *defaultMeta, - }, - Message: "successfully bound route", - }}, + }), + }, }, }, &structs.TCPRouteConfigEntry{ @@ -793,22 +2019,14 @@ func TestAPIGatewayController(t *testing.T) { Name: "tcp-route-two", EnterpriseMeta: *defaultMeta, Status: structs.Status{ - Conditions: []structs.Condition{{ - Type: "Accepted", - Status: "True", - Reason: "Accepted", - Message: "route is valid", - }, { - Type: "Bound", - Status: "True", - Reason: "Bound", - Resource: &structs.ResourceReference{ + Conditions: []structs.Condition{ + conditions.routeAccepted(), + conditions.routeBound(structs.ResourceReference{ Kind: structs.APIGateway, Name: "gateway", EnterpriseMeta: *defaultMeta, - }, - Message: "successfully bound route", - }}, + }), + }, }, }, }, @@ -903,23 +2121,15 @@ func TestAPIGatewayController(t *testing.T) { Name: "gateway", EnterpriseMeta: *defaultMeta, Status: structs.Status{ - Conditions: []structs.Condition{{ - Type: "Accepted", - Status: "True", - Reason: "Accepted", - Message: "gateway is valid", - }, { - Type: "Conflicted", - Status: "False", - Reason: "NoConflict", - Resource: &structs.ResourceReference{ + Conditions: []structs.Condition{ + conditions.gatewayAccepted(), + conditions.gatewayListenerNoConflicts(structs.ResourceReference{ Kind: structs.APIGateway, Name: "gateway", EnterpriseMeta: *defaultMeta, SectionName: "listener", - }, - Message: "listener has no route conflicts", - }}, + }), + }, }, }, &structs.HTTPRouteConfigEntry{ @@ -927,22 +2137,14 @@ func TestAPIGatewayController(t *testing.T) { Name: "http-route-one", EnterpriseMeta: *defaultMeta, Status: structs.Status{ - Conditions: []structs.Condition{{ - Type: "Accepted", - Status: "True", - Reason: "Accepted", - Message: "route is valid", - }, { - Type: "Bound", - Status: "True", - Reason: "Bound", - Resource: &structs.ResourceReference{ + Conditions: []structs.Condition{ + conditions.routeAccepted(), + conditions.routeBound(structs.ResourceReference{ Kind: structs.APIGateway, Name: "gateway", EnterpriseMeta: *defaultMeta, - }, - Message: "successfully bound route", - }}, + }), + }, }, }, &structs.HTTPRouteConfigEntry{ @@ -950,22 +2152,14 @@ func TestAPIGatewayController(t *testing.T) { Name: "http-route-two", EnterpriseMeta: *defaultMeta, Status: structs.Status{ - Conditions: []structs.Condition{{ - Type: "Accepted", - Status: "True", - Reason: "Accepted", - Message: "route is valid", - }, { - Type: "Bound", - Status: "True", - Reason: "Bound", - Resource: &structs.ResourceReference{ + Conditions: []structs.Condition{ + conditions.routeAccepted(), + conditions.routeBound(structs.ResourceReference{ Kind: structs.APIGateway, Name: "gateway", EnterpriseMeta: *defaultMeta, - }, - Message: "successfully bound route", - }}, + }), + }, }, }, }, @@ -1060,23 +2254,15 @@ func TestAPIGatewayController(t *testing.T) { Name: "gateway", EnterpriseMeta: *defaultMeta, Status: structs.Status{ - Conditions: []structs.Condition{{ - Type: "Accepted", - Status: "True", - Reason: "Accepted", - Message: "gateway is valid", - }, { - Type: "Conflicted", - Status: "False", - Reason: "NoConflict", - Resource: &structs.ResourceReference{ + Conditions: []structs.Condition{ + conditions.gatewayAccepted(), + conditions.gatewayListenerNoConflicts(structs.ResourceReference{ Kind: structs.APIGateway, Name: "gateway", EnterpriseMeta: *defaultMeta, SectionName: "listener", - }, - Message: "listener has no route conflicts", - }}, + }), + }, }, }, &structs.HTTPRouteConfigEntry{ @@ -1084,22 +2270,14 @@ func TestAPIGatewayController(t *testing.T) { Name: "http-route", EnterpriseMeta: *defaultMeta, Status: structs.Status{ - Conditions: []structs.Condition{{ - Type: "Accepted", - Status: "True", - Reason: "Accepted", - Message: "route is valid", - }, { - Type: "Bound", - Status: "True", - Reason: "Bound", - Resource: &structs.ResourceReference{ + Conditions: []structs.Condition{ + conditions.routeAccepted(), + conditions.routeBound(structs.ResourceReference{ Kind: structs.APIGateway, Name: "gateway", EnterpriseMeta: *defaultMeta, - }, - Message: "successfully bound route", - }}, + }), + }, }, }, &structs.TCPRouteConfigEntry{ @@ -1107,22 +2285,14 @@ func TestAPIGatewayController(t *testing.T) { Name: "tcp-route", EnterpriseMeta: *defaultMeta, Status: structs.Status{ - Conditions: []structs.Condition{{ - Type: "Accepted", - Status: "True", - Reason: "Accepted", - Message: "route is valid", - }, { - Type: "Bound", - Status: "False", - Reason: "FailedToBind", - Resource: &structs.ResourceReference{ + Conditions: []structs.Condition{ + conditions.routeAccepted(), + conditions.routeUnbound(structs.ResourceReference{ Kind: structs.APIGateway, Name: "gateway", EnterpriseMeta: *defaultMeta, - }, - Message: "failed to bind route tcp-route to gateway gateway: no valid listener has name '' and uses tcp protocol", - }}, + }, errors.New("failed to bind route tcp-route to gateway gateway with listener ''")), + }, }, }, }, @@ -1228,32 +2398,19 @@ func TestAPIGatewayController(t *testing.T) { Name: "gateway", EnterpriseMeta: *defaultMeta, Status: structs.Status{ - Conditions: []structs.Condition{{ - Type: "Accepted", - Status: "True", - Reason: "Accepted", - Message: "gateway is valid", - }, { - Type: "Conflicted", - Status: "False", - Reason: "NoConflict", - Resource: &structs.ResourceReference{ + Conditions: []structs.Condition{ + conditions.gatewayAccepted(), + conditions.gatewayListenerNoConflicts(structs.ResourceReference{ Kind: structs.APIGateway, Name: "gateway", SectionName: "http-listener", - }, - Message: "listener has no route conflicts", - }, { - Type: "Conflicted", - Status: "False", - Reason: "NoConflict", - Resource: &structs.ResourceReference{ + }), + conditions.gatewayListenerNoConflicts(structs.ResourceReference{ Kind: structs.APIGateway, Name: "gateway", SectionName: "tcp-listener", - }, - Message: "listener has no route conflicts", - }}, + }), + }, }, }, &structs.HTTPRouteConfigEntry{ @@ -1261,22 +2418,14 @@ func TestAPIGatewayController(t *testing.T) { Name: "http-route", EnterpriseMeta: *defaultMeta, Status: structs.Status{ - Conditions: []structs.Condition{{ - Type: "Accepted", - Status: "True", - Reason: "Accepted", - Message: "route is valid", - }, { - Type: "Bound", - Status: "True", - Reason: "Bound", - Resource: &structs.ResourceReference{ + Conditions: []structs.Condition{ + conditions.routeAccepted(), + conditions.routeBound(structs.ResourceReference{ Kind: structs.APIGateway, Name: "gateway", EnterpriseMeta: *defaultMeta, - }, - Message: "successfully bound route", - }}, + }), + }, }, }, &structs.TCPRouteConfigEntry{ @@ -1284,22 +2433,14 @@ func TestAPIGatewayController(t *testing.T) { Name: "tcp-route", EnterpriseMeta: *defaultMeta, Status: structs.Status{ - Conditions: []structs.Condition{{ - Type: "Accepted", - Status: "True", - Reason: "Accepted", - Message: "route is valid", - }, { - Type: "Bound", - Status: "True", - Reason: "Bound", - Resource: &structs.ResourceReference{ + Conditions: []structs.Condition{ + conditions.routeAccepted(), + conditions.routeBound(structs.ResourceReference{ Kind: structs.APIGateway, Name: "gateway", EnterpriseMeta: *defaultMeta, - }, - Message: "successfully bound route", - }}, + }), + }, }, }, }, @@ -1319,22 +2460,14 @@ func TestAPIGatewayController(t *testing.T) { Name: "tcp-upstream", }}, Status: structs.Status{ - Conditions: []structs.Condition{{ - Type: "Accepted", - Status: "True", - Reason: "Accepted", - Message: "route is valid", - }, { - Type: "Bound", - Status: "True", - Reason: "Bound", - Resource: &structs.ResourceReference{ + Conditions: []structs.Condition{ + conditions.routeAccepted(), + conditions.routeBound(structs.ResourceReference{ Kind: structs.APIGateway, Name: "gateway", EnterpriseMeta: *defaultMeta, - }, - Message: "successfully bound route", - }}, + }), + }, }, }, &structs.ServiceConfigEntry{ @@ -1381,18 +2514,25 @@ func TestAPIGatewayController(t *testing.T) { Kind: structs.APIGateway, Name: "gateway", EnterpriseMeta: *defaultMeta, + Status: structs.Status{ + Conditions: []structs.Condition{ + conditions.gatewayListenerNoConflicts(structs.ResourceReference{ + Kind: structs.APIGateway, + Name: "gateway", + SectionName: "tcp-listener", + EnterpriseMeta: *defaultMeta, + }), + }, + }, }, &structs.TCPRouteConfigEntry{ Kind: structs.TCPRoute, Name: "tcp-route", EnterpriseMeta: *defaultMeta, Status: structs.Status{ - Conditions: []structs.Condition{{ - Type: "Accepted", - Status: "True", - Reason: "Accepted", - Message: "route is valid", - }}, + Conditions: []structs.Condition{ + conditions.routeAccepted(), + }, }, }, }, @@ -1409,22 +2549,14 @@ func TestAPIGatewayController(t *testing.T) { Name: "tcp-route", EnterpriseMeta: *defaultMeta, Status: structs.Status{ - Conditions: []structs.Condition{{ - Type: "Accepted", - Status: "True", - Reason: "Accepted", - Message: "route is valid", - }, { - Type: "Bound", - Status: "True", - Reason: "Bound", - Resource: &structs.ResourceReference{ + Conditions: []structs.Condition{ + conditions.routeAccepted(), + conditions.routeBound(structs.ResourceReference{ Kind: structs.APIGateway, Name: "gateway", EnterpriseMeta: *defaultMeta, - }, - Message: "successfully bound route", - }}, + }), + }, }, }, &structs.ServiceConfigEntry{ @@ -1471,18 +2603,25 @@ func TestAPIGatewayController(t *testing.T) { Kind: structs.APIGateway, Name: "gateway", EnterpriseMeta: *defaultMeta, + Status: structs.Status{ + Conditions: []structs.Condition{ + conditions.gatewayListenerNoConflicts(structs.ResourceReference{ + Kind: structs.APIGateway, + Name: "gateway", + SectionName: "tcp-listener", + EnterpriseMeta: *defaultMeta, + }), + }, + }, }, &structs.TCPRouteConfigEntry{ Kind: structs.TCPRoute, Name: "tcp-route", EnterpriseMeta: *defaultMeta, Status: structs.Status{ - Conditions: []structs.Condition{{ - Type: "Accepted", - Status: "False", - Reason: "NoUpstreamServicesTargeted", - Message: "route must target at least one upstream service", - }}, + Conditions: []structs.Condition{ + conditions.routeNoUpstreams(), + }, }, }, }, @@ -1508,22 +2647,14 @@ func TestAPIGatewayController(t *testing.T) { EnterpriseMeta: *defaultMeta, }}, Status: structs.Status{ - Conditions: []structs.Condition{{ - Type: "Accepted", - Status: "True", - Reason: "Accepted", - Message: "route is valid", - }, { - Type: "Bound", - Status: "True", - Reason: "Bound", - Resource: &structs.ResourceReference{ + Conditions: []structs.Condition{ + conditions.routeAccepted(), + conditions.routeBound(structs.ResourceReference{ Kind: structs.APIGateway, Name: "gateway", EnterpriseMeta: *defaultMeta, - }, - Message: "successfully bound route", - }}, + }), + }, }, }, &structs.HTTPRouteConfigEntry{ @@ -1542,20 +2673,13 @@ func TestAPIGatewayController(t *testing.T) { EnterpriseMeta: *defaultMeta, }}, Status: structs.Status{ - Conditions: []structs.Condition{{ - Type: "Accepted", - Status: "True", - Reason: "Accepted", - Message: "route is valid", - }, { - Type: "Bound", - Status: "False", - Reason: "NotBound", - Resource: &structs.ResourceReference{ + Conditions: []structs.Condition{ + conditions.routeAccepted(), + conditions.routeUnbound(structs.ResourceReference{ Kind: structs.APIGateway, Name: "gateway", - }, - }}, + }, errors.New("foo")), + }, }, }, &structs.ServiceConfigEntry{ @@ -1580,22 +2704,14 @@ func TestAPIGatewayController(t *testing.T) { Port: 80, }}, Status: structs.Status{ - Conditions: []structs.Condition{{ - Type: "Accepted", - Status: "True", - Reason: "Accepted", - Message: "gateway is valid", - }, { - Type: "Conflicted", - Status: "False", - Reason: "NoConflict", - Resource: &structs.ResourceReference{ + Conditions: []structs.Condition{ + conditions.gatewayAccepted(), + conditions.gatewayListenerNoConflicts(structs.ResourceReference{ Kind: structs.APIGateway, Name: "gateway", SectionName: "tcp-listener", - }, - Message: "listener has no route conflicts", - }}, + }), + }, }, }, &structs.BoundAPIGatewayConfigEntry{ @@ -1631,22 +2747,14 @@ func TestAPIGatewayController(t *testing.T) { Name: "gateway", EnterpriseMeta: *defaultMeta, Status: structs.Status{ - Conditions: []structs.Condition{{ - Type: "Accepted", - Status: "True", - Reason: "Accepted", - Message: "gateway is valid", - }, { - Type: "Conflicted", - Status: "False", - Reason: "NoConflict", - Resource: &structs.ResourceReference{ + Conditions: []structs.Condition{ + conditions.gatewayAccepted(), + conditions.gatewayListenerNoConflicts(structs.ResourceReference{ Kind: structs.APIGateway, Name: "gateway", SectionName: "http-listener", - }, - Message: "listener has no route conflicts", - }}, + }), + }, }, }, &structs.TCPRouteConfigEntry{ @@ -1654,21 +2762,13 @@ func TestAPIGatewayController(t *testing.T) { Name: "tcp-route", EnterpriseMeta: *defaultMeta, Status: structs.Status{ - Conditions: []structs.Condition{{ - Type: "Accepted", - Status: "True", - Reason: "Accepted", - Message: "route is valid", - }, { - Type: "Bound", - Status: "False", - Reason: "FailedToBind", - Message: "failed to bind route tcp-route to gateway gateway: no valid listener has name '' and uses tcp protocol", - Resource: &structs.ResourceReference{ + Conditions: []structs.Condition{ + conditions.routeAccepted(), + conditions.routeUnbound(structs.ResourceReference{ Kind: structs.APIGateway, Name: "gateway", - }, - }}, + }, errors.New("failed to bind route tcp-route to gateway gateway with listener ''")), + }, }, }, &structs.HTTPRouteConfigEntry{ @@ -1676,21 +2776,13 @@ func TestAPIGatewayController(t *testing.T) { Name: "http-route", EnterpriseMeta: *defaultMeta, Status: structs.Status{ - Conditions: []structs.Condition{{ - Type: "Accepted", - Status: "True", - Reason: "Accepted", - Message: "route is valid", - }, { - Type: "Bound", - Status: "True", - Reason: "Bound", - Message: "successfully bound route", - Resource: &structs.ResourceReference{ + Conditions: []structs.Condition{ + conditions.routeAccepted(), + conditions.routeBound(structs.ResourceReference{ Kind: structs.APIGateway, Name: "gateway", - }, - }}, + }), + }, }, }, }, @@ -1720,22 +2812,14 @@ func TestAPIGatewayController(t *testing.T) { EnterpriseMeta: *defaultMeta, }}, Status: structs.Status{ - Conditions: []structs.Condition{{ - Type: "Accepted", - Status: "True", - Reason: "Accepted", - Message: "route is valid", - }, { - Type: "Bound", - Status: "True", - Reason: "Bound", - Resource: &structs.ResourceReference{ + Conditions: []structs.Condition{ + conditions.routeAccepted(), + conditions.routeBound(structs.ResourceReference{ Kind: structs.APIGateway, Name: "gateway", EnterpriseMeta: *defaultMeta, - }, - Message: "successfully bound route", - }}, + }), + }, }, }, &structs.ServiceConfigEntry{ @@ -1764,21 +2848,13 @@ func TestAPIGatewayController(t *testing.T) { Name: "tcp-route", EnterpriseMeta: *defaultMeta, Status: structs.Status{ - Conditions: []structs.Condition{{ - Type: "Accepted", - Status: "True", - Reason: "Accepted", - Message: "route is valid", - }, { - Type: "Bound", - Status: "False", - Reason: "GatewayNotFound", - Message: "gateway was not found", - Resource: &structs.ResourceReference{ + Conditions: []structs.Condition{ + conditions.routeAccepted(), + conditions.gatewayNotFound(structs.ResourceReference{ Kind: structs.APIGateway, Name: "gateway", - }, - }}, + }), + }, }, }, }, @@ -1813,22 +2889,14 @@ func TestAPIGatewayController(t *testing.T) { Protocol: structs.ListenerProtocolTCP, }}, Status: structs.Status{ - Conditions: []structs.Condition{{ - Type: "Accepted", - Status: "True", - Reason: "Accepted", - Message: "gateway is valid", - }, { - Type: "Conflicted", - Status: "False", - Reason: "NoConflict", - Resource: &structs.ResourceReference{ + Conditions: []structs.Condition{ + conditions.gatewayAccepted(), + conditions.gatewayListenerNoConflicts(structs.ResourceReference{ Kind: structs.APIGateway, Name: "gateway", SectionName: "tcp-listener", - }, - Message: "listener has no route conflicts", - }}, + }), + }, }, }, }, @@ -1847,22 +2915,14 @@ func TestAPIGatewayController(t *testing.T) { Name: "gateway", EnterpriseMeta: *defaultMeta, Status: structs.Status{ - Conditions: []structs.Condition{{ - Type: "Accepted", - Status: "True", - Reason: "Accepted", - Message: "gateway is valid", - }, { - Type: "Conflicted", - Status: "False", - Reason: "NoConflict", - Resource: &structs.ResourceReference{ + Conditions: []structs.Condition{ + conditions.gatewayAccepted(), + conditions.gatewayListenerNoConflicts(structs.ResourceReference{ Kind: structs.APIGateway, Name: "gateway", SectionName: "tcp-listener", - }, - Message: "listener has no route conflicts", - }}, + }), + }, }, }, }, @@ -1900,20 +2960,13 @@ func TestAPIGatewayController(t *testing.T) { Name: "gateway", }}, Status: structs.Status{ - Conditions: []structs.Condition{{ - Type: "Accepted", - Status: "True", - Reason: "Accepted", - Message: "route is valid", - }, { - Type: "Bound", - Status: "True", - Reason: "Bound", - Resource: &structs.ResourceReference{ + Conditions: []structs.Condition{ + conditions.routeAccepted(), + conditions.routeBound(structs.ResourceReference{ Kind: structs.APIGateway, Name: "gateway", - }, - }}, + }), + }, }, }, }, @@ -1923,21 +2976,13 @@ func TestAPIGatewayController(t *testing.T) { Name: "tcp-route", EnterpriseMeta: *defaultMeta, Status: structs.Status{ - Conditions: []structs.Condition{{ - Type: "Accepted", - Status: "True", - Reason: "Accepted", - Message: "route is valid", - }, { - Type: "Bound", - Status: "False", - Reason: "GatewayNotFound", - Message: "gateway was not found", - Resource: &structs.ResourceReference{ + Conditions: []structs.Condition{ + conditions.routeAccepted(), + conditions.gatewayNotFound(structs.ResourceReference{ Kind: structs.APIGateway, Name: "gateway", - }, - }}, + }), + }, }, }, }, @@ -1981,31 +3026,18 @@ func TestAPIGatewayController(t *testing.T) { }, }}, Status: structs.Status{ - Conditions: []structs.Condition{{ - Type: "Accepted", - Status: "False", - Reason: "InvalidCertificate", - Message: "certificate not found", - Resource: &structs.ResourceReference{ + Conditions: []structs.Condition{ + conditions.invalidCertificate(structs.ResourceReference{ Kind: structs.InlineCertificate, Name: "certificate", - }, - }, { - Type: "Accepted", - Status: "False", - Reason: "InvalidCertificates", - Message: "gateway references invalid certificates", - }, { - Type: "Conflicted", - Status: "False", - Reason: "NoConflict", - Message: "listener has no route conflicts", - Resource: &structs.ResourceReference{ + }, errors.New("certificate not found")), + conditions.invalidCertificates(), + conditions.gatewayListenerNoConflicts(structs.ResourceReference{ Kind: structs.APIGateway, Name: "gateway", SectionName: "http-listener", - }, - }}, + }), + }, }, }, &structs.BoundAPIGatewayConfigEntry{ @@ -2062,23 +3094,15 @@ func TestAPIGatewayController(t *testing.T) { }, }}, Status: structs.Status{ - Conditions: []structs.Condition{{ - Type: "Accepted", - Status: "True", - Reason: "Accepted", - Message: "gateway is valid", - }, { - Type: "Conflicted", - Status: "False", - Reason: "NoConflict", - Message: "listener has no route conflicts", - Resource: &structs.ResourceReference{ + Conditions: []structs.Condition{ + conditions.gatewayAccepted(), + conditions.gatewayListenerNoConflicts(structs.ResourceReference{ Kind: structs.APIGateway, Name: "gateway", SectionName: "http-listener", - }, + }), }, - }}, + }, }, &structs.BoundAPIGatewayConfigEntry{ Kind: structs.BoundAPIGateway, @@ -2086,6 +3110,11 @@ func TestAPIGatewayController(t *testing.T) { EnterpriseMeta: *defaultMeta, Listeners: []structs.BoundAPIGatewayListener{{ Name: "http-listener", + Certificates: []structs.ResourceReference{{ + Kind: structs.InlineCertificate, + Name: "certificate", + EnterpriseMeta: *defaultMeta, + }}, }}, }, }, @@ -2112,31 +3141,18 @@ func TestAPIGatewayController(t *testing.T) { }, }}, Status: structs.Status{ - Conditions: []structs.Condition{{ - Type: "Accepted", - Status: "False", - Reason: "InvalidCertificate", - Message: "certificate not found", - Resource: &structs.ResourceReference{ + Conditions: []structs.Condition{ + conditions.invalidCertificate(structs.ResourceReference{ Kind: structs.InlineCertificate, Name: "certificate", - }, - }, { - Type: "Accepted", - Status: "False", - Reason: "InvalidCertificates", - Message: "gateway references invalid certificates", - }, { - Type: "Conflicted", - Status: "False", - Reason: "NoConflict", - Message: "listener has no route conflicts", - Resource: &structs.ResourceReference{ + }, errors.New("certificate not found")), + conditions.invalidCertificates(), + conditions.gatewayListenerNoConflicts(structs.ResourceReference{ Kind: structs.APIGateway, Name: "gateway", SectionName: "http-listener", - }, - }}, + }), + }, }, }, &structs.InlineCertificateConfigEntry{ @@ -2161,23 +3177,15 @@ func TestAPIGatewayController(t *testing.T) { }, }}, Status: structs.Status{ - Conditions: []structs.Condition{{ - Type: "Accepted", - Status: "True", - Reason: "Accepted", - Message: "gateway is valid", - }, { - Type: "Conflicted", - Status: "False", - Reason: "NoConflict", - Message: "listener has no route conflicts", - Resource: &structs.ResourceReference{ + Conditions: []structs.Condition{ + conditions.gatewayAccepted(), + conditions.gatewayListenerNoConflicts(structs.ResourceReference{ Kind: structs.APIGateway, Name: "gateway", SectionName: "http-listener", - }, + }), }, - }}, + }, }, &structs.BoundAPIGatewayConfigEntry{ Kind: structs.BoundAPIGateway, @@ -2185,6 +3193,11 @@ func TestAPIGatewayController(t *testing.T) { EnterpriseMeta: *defaultMeta, Listeners: []structs.BoundAPIGatewayListener{{ Name: "http-listener", + Certificates: []structs.ResourceReference{{ + Kind: structs.InlineCertificate, + Name: "certificate", + EnterpriseMeta: *defaultMeta, + }}, }}, }, }, @@ -2358,7 +3371,7 @@ func TestAPIGatewayController(t *testing.T) { require.NoError(t, err) ppExpected, err := json.MarshalIndent(expected, "", " ") require.NoError(t, err) - require.False(t, stateIsDirty(bound, expected.(*structs.BoundAPIGatewayConfigEntry)), fmt.Sprintf("api bound states do not match: %+v != %+v", string(ppActual), string(ppExpected))) + require.True(t, bound.IsSame(expected.(*structs.BoundAPIGatewayConfigEntry)), fmt.Sprintf("api bound states do not match: %+v != %+v", string(ppActual), string(ppExpected))) } found = true break diff --git a/agent/consul/gateways/gateway_meta.go b/agent/consul/gateways/gateway_meta.go deleted file mode 100644 index dca6e8c9f9..0000000000 --- a/agent/consul/gateways/gateway_meta.go +++ /dev/null @@ -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 -} diff --git a/agent/consul/gateways/gateway_meta_test.go b/agent/consul/gateways/gateway_meta_test.go deleted file mode 100644 index 3191f38372..0000000000 --- a/agent/consul/gateways/gateway_meta_test.go +++ /dev/null @@ -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) - }) - } -} diff --git a/agent/structs/config_entry_gateways.go b/agent/structs/config_entry_gateways.go index 6757775b41..449f56b337 100644 --- a/agent/structs/config_entry_gateways.go +++ b/agent/structs/config_entry_gateways.go @@ -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}