From e69e7fd1f29d9438c7283a81fc54c8a029555783 Mon Sep 17 00:00:00 2001 From: Thomas Eckert Date: Fri, 27 Jan 2023 09:41:03 -0500 Subject: [PATCH] Match route and listener protocols when binding (#16057) * Add GatewayMeta for matching routes to listeners based on protocols * Add GetGatewayMeta * Apply suggestions from code review Co-authored-by: Nathan Coleman * Make GatewayMeta private * Bound -> BoundGateway * Document gatewayMeta more * Simplify conditional * Parallelize tests and simplify bind conditional * gofmt * :droplet: getGatewayMeta --------- Co-authored-by: Nathan Coleman --- agent/consul/gateways/bind.go | 14 +- agent/consul/gateways/bind_test.go | 1009 +++++++++++++++---- agent/consul/gateways/gateway_meta.go | 139 +++ agent/consul/gateways/gateway_meta_test.go | 373 +++++++ agent/structs/config_entry_gateways.go | 76 +- agent/structs/config_entry_gateways_test.go | 284 ------ agent/structs/config_entry_routes.go | 31 +- 7 files changed, 1347 insertions(+), 579 deletions(-) create mode 100644 agent/consul/gateways/gateway_meta.go create mode 100644 agent/consul/gateways/gateway_meta_test.go diff --git a/agent/consul/gateways/bind.go b/agent/consul/gateways/bind.go index bed26b713..ede1de038 100644 --- a/agent/consul/gateways/bind.go +++ b/agent/consul/gateways/bind.go @@ -21,7 +21,7 @@ type gatewayRefs = map[configentry.KindName][]structs.ResourceReference // The function returns a list of references to the modified BoundAPIGatewayConfigEntry objects, // a map of resource references to errors that occurred when they were attempted to be // bound to a gateway. -func BindRoutesToGateways(gateways []*structs.BoundAPIGatewayConfigEntry, routes ...structs.BoundRoute) ([]*structs.BoundAPIGatewayConfigEntry, map[structs.ResourceReference]error) { +func BindRoutesToGateways(gateways []*gatewayMeta, routes ...structs.BoundRoute) ([]*structs.BoundAPIGatewayConfigEntry, map[structs.ResourceReference]error) { modified := make([]*structs.BoundAPIGatewayConfigEntry, 0, len(gateways)) // errored stores the errors from events where a resource reference failed to bind to a gateway. @@ -32,11 +32,11 @@ func BindRoutesToGateways(gateways []*structs.BoundAPIGatewayConfigEntry, routes // 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)] + references, routeReferencesGateway := gatewayRefs[configentry.NewKindNameForEntry(gateway.BoundGateway)] if routeReferencesGateway { - didUpdate, errors := gateway.UpdateRouteBinding(references, route) + didUpdate, errors := gateway.updateRouteBinding(references, route) if didUpdate { - modified = append(modified, gateway) + modified = append(modified, gateway.BoundGateway) } for ref, err := range errors { errored[ref] = err @@ -44,10 +44,8 @@ func BindRoutesToGateways(gateways []*structs.BoundAPIGatewayConfigEntry, routes for _, ref := range references { delete(parentRefs, ref) } - } else { - if gateway.UnbindRoute(route) { - modified = append(modified, gateway) - } + } else if gateway.unbindRoute(route) { + modified = append(modified, gateway.BoundGateway) } } diff --git a/agent/consul/gateways/bind_test.go b/agent/consul/gateways/bind_test.go index 0b6c6c919..74f030f7a 100644 --- a/agent/consul/gateways/bind_test.go +++ b/agent/consul/gateways/bind_test.go @@ -1,6 +1,7 @@ package gateways import ( + "fmt" "testing" "github.com/hashicorp/consul/agent/structs" @@ -8,8 +9,10 @@ import ( ) func TestBindRoutesToGateways(t *testing.T) { + t.Parallel() + type testCase struct { - gateways []*structs.BoundAPIGatewayConfigEntry + gateways []*gatewayMeta routes []structs.BoundRoute expectedBoundAPIGateways []*structs.BoundAPIGatewayConfigEntry expectedReferenceErrors map[structs.ResourceReference]error @@ -17,245 +20,873 @@ func TestBindRoutesToGateways(t *testing.T) { cases := map[string]testCase{ "TCP Route binds to gateway": { - gateways: []*structs.BoundAPIGatewayConfigEntry{ - makeGateway("Test Bound API Gateway", []structs.BoundAPIGatewayListener{ - makeListener("Test Listener", []structs.ResourceReference{}), - }), + 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{ - makeRoute(structs.TCPRoute, "Test TCP Route", []structs.ResourceReference{ - makeRef(structs.APIGateway, "Test Bound API Gateway", "Test Listener"), - }), + &structs.TCPRouteConfigEntry{ + Kind: structs.TCPRoute, + Name: "TCP Route", + Parents: []structs.ResourceReference{ + { + Name: "Gateway", + Kind: structs.APIGateway, + SectionName: "Listener", + }, + }, + }, }, expectedBoundAPIGateways: []*structs.BoundAPIGatewayConfigEntry{ - makeGateway("Test Bound API Gateway", []structs.BoundAPIGatewayListener{ - makeListener("Test Listener", []structs.ResourceReference{ - makeRef(structs.TCPRoute, "Test TCP Route", ""), - }), - }), + { + 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: []*structs.BoundAPIGatewayConfigEntry{ - makeGateway("Test Bound API Gateway", []structs.BoundAPIGatewayListener{ - makeListener("Test Listener", []structs.ResourceReference{ - makeRef(structs.TCPRoute, "Test TCP Route", ""), - }), - }), - makeGateway("Other Test Bound API Gateway", []structs.BoundAPIGatewayListener{ - makeListener("Test Listener", []structs.ResourceReference{ - makeRef(structs.TCPRoute, "Test TCP Route", ""), - }), - }), + 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{ - makeRoute(structs.TCPRoute, "Test TCP Route", []structs.ResourceReference{ - makeRef(structs.APIGateway, "Test Bound API Gateway", "Test Listener"), - }), + &structs.TCPRouteConfigEntry{ + Kind: structs.TCPRoute, + Name: "TCP Route", + Parents: []structs.ResourceReference{}, + }, }, expectedBoundAPIGateways: []*structs.BoundAPIGatewayConfigEntry{ - makeGateway("Test Bound API Gateway", []structs.BoundAPIGatewayListener{ - makeListener("Test Listener", []structs.ResourceReference{ - makeRef(structs.TCPRoute, "Test TCP Route", ""), - }), - }), - makeGateway("Other Test Bound API Gateway", []structs.BoundAPIGatewayListener{ - makeListener("Test Listener", []structs.ResourceReference{}), - }), + { + Name: "Gateway", + Listeners: []structs.BoundAPIGatewayListener{ + { + Name: "Listener", + Routes: []structs.ResourceReference{}, + }, + }, + }, }, expectedReferenceErrors: map[structs.ResourceReference]error{}, }, "TCP Route binds to multiple gateways": { - gateways: []*structs.BoundAPIGatewayConfigEntry{ - makeGateway("Test Bound API Gateway", []structs.BoundAPIGatewayListener{ - makeListener("Test Listener", []structs.ResourceReference{}), - }), - makeGateway("Other Test Bound API Gateway", []structs.BoundAPIGatewayListener{ - makeListener("Test Listener", []structs.ResourceReference{}), - }), + 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{ - makeRoute(structs.TCPRoute, "Test TCP Route", []structs.ResourceReference{ - makeRef(structs.APIGateway, "Test Bound API Gateway", "Test Listener"), - makeRef(structs.APIGateway, "Other Test Bound API Gateway", "Test Listener"), - }), + &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{ - makeGateway("Test Bound API Gateway", []structs.BoundAPIGatewayListener{ - makeListener("Test Listener", []structs.ResourceReference{ - makeRef(structs.TCPRoute, "Test TCP Route", ""), - }), - }), - makeGateway("Other Test Bound API Gateway", []structs.BoundAPIGatewayListener{ - makeListener("Test Listener", []structs.ResourceReference{ - makeRef(structs.TCPRoute, "Test TCP Route", ""), - }), - }), + { + 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 gateway with multiple listeners": { - gateways: []*structs.BoundAPIGatewayConfigEntry{ - makeGateway("Test Bound API Gateway", []structs.BoundAPIGatewayListener{ - makeListener("Test Listener", []structs.ResourceReference{}), - makeListener("Other Test Listener", []structs.ResourceReference{}), - }), + "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{ - makeRoute(structs.TCPRoute, "Test TCP Route", []structs.ResourceReference{ - makeRef(structs.APIGateway, "Test Bound API Gateway", "Test Listener"), - }), + &structs.TCPRouteConfigEntry{ + Name: "TCP Route", + Kind: structs.TCPRoute, + Parents: []structs.ResourceReference{ + { + Name: "Gateway", + Kind: structs.APIGateway, + SectionName: "Listener 2", + }, + }, + }, }, expectedBoundAPIGateways: []*structs.BoundAPIGatewayConfigEntry{ - makeGateway("Test Bound API Gateway", []structs.BoundAPIGatewayListener{ - makeListener("Test Listener", []structs.ResourceReference{ - makeRef(structs.TCPRoute, "Test TCP Route", ""), - }), - makeListener("Other Test Listener", []structs.ResourceReference{}), - }), + { + 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: []*structs.BoundAPIGatewayConfigEntry{ - makeGateway("Test Bound API Gateway", []structs.BoundAPIGatewayListener{ - makeListener("Test Listener", []structs.ResourceReference{}), - makeListener("Other Test Listener", []structs.ResourceReference{}), - }), + 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{ - makeRoute(structs.TCPRoute, "Test TCP Route", []structs.ResourceReference{ - makeRef(structs.APIGateway, "Test Bound API Gateway", ""), - }), + &structs.TCPRouteConfigEntry{ + Name: "TCP Route", + Kind: structs.TCPRoute, + Parents: []structs.ResourceReference{ + { + Name: "Gateway", + Kind: structs.APIGateway, + SectionName: "", + }, + }, + }, }, expectedBoundAPIGateways: []*structs.BoundAPIGatewayConfigEntry{ - makeGateway("Test Bound API Gateway", []structs.BoundAPIGatewayListener{ - makeListener("Test Listener", []structs.ResourceReference{ - makeRef(structs.TCPRoute, "Test TCP Route", ""), - }), - makeListener("Other Test Listener", []structs.ResourceReference{ - makeRef(structs.TCPRoute, "Test TCP Route", ""), - }), - }), + { + 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: []*structs.BoundAPIGatewayConfigEntry{ - makeGateway("Test Bound API Gateway", []structs.BoundAPIGatewayListener{ - makeListener("Test Listener", []structs.ResourceReference{}), - makeListener("Other Test Listener", []structs.ResourceReference{ - makeRef(structs.TCPRoute, "Test TCP Route", ""), - }), - }), + 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{ - makeRoute(structs.TCPRoute, "Test TCP Route", []structs.ResourceReference{ - makeRef(structs.APIGateway, "Test Bound API Gateway", "Test Listener"), - makeRef(structs.APIGateway, "Test Bound API Gateway", "Other Test Listener"), - }), + &structs.TCPRouteConfigEntry{ + Name: "TCP Route", + Kind: structs.TCPRoute, + Parents: []structs.ResourceReference{ + { + Name: "Gateway", + Kind: structs.APIGateway, + SectionName: "", + }, + }, + }, }, expectedBoundAPIGateways: []*structs.BoundAPIGatewayConfigEntry{ - makeGateway("Test Bound API Gateway", []structs.BoundAPIGatewayListener{ - makeListener("Test Listener", []structs.ResourceReference{ - makeRef(structs.TCPRoute, "Test TCP Route", ""), - }), - makeListener("Other Test Listener", []structs.ResourceReference{ - makeRef(structs.TCPRoute, "Test TCP Route", ""), - }), - }), + { + 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: []*structs.BoundAPIGatewayConfigEntry{ - makeGateway("Test Bound API Gateway", []structs.BoundAPIGatewayListener{ - makeListener("Test Listener", []structs.ResourceReference{}), - makeListener("Other Test Listener", []structs.ResourceReference{}), - }), - makeGateway("Other Test Bound API Gateway", []structs.BoundAPIGatewayListener{ - makeListener("Test Listener", []structs.ResourceReference{}), - makeListener("Other Test Listener", []structs.ResourceReference{}), - }), + 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{ - makeRoute(structs.TCPRoute, "Test TCP Route", []structs.ResourceReference{ - makeRef(structs.APIGateway, "Test Bound API Gateway", "Test Listener"), - makeRef(structs.APIGateway, "Other Test Bound API Gateway", "Test Listener"), - }), + &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{ - makeGateway("Test Bound API Gateway", []structs.BoundAPIGatewayListener{ - makeListener("Test Listener", []structs.ResourceReference{ - makeRef(structs.TCPRoute, "Test TCP Route", ""), - }), - makeListener("Other Test Listener", []structs.ResourceReference{}), - }), - makeGateway("Other Test Bound API Gateway", []structs.BoundAPIGatewayListener{ - makeListener("Test Listener", []structs.ResourceReference{ - makeRef(structs.TCPRoute, "Test TCP Route", ""), - }), - makeListener("Other Test Listener", []structs.ResourceReference{}), - }), + { + 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: []*structs.BoundAPIGatewayConfigEntry{ - makeGateway("Test Bound API Gateway", []structs.BoundAPIGatewayListener{ - makeListener("Test Listener", []structs.ResourceReference{ - makeRef(structs.TCPRoute, "Test TCP Route", ""), - }), - makeListener("Other Test Listener", []structs.ResourceReference{}), - }), + 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{ - makeRoute(structs.TCPRoute, "Test TCP Route", []structs.ResourceReference{ - makeRef(structs.APIGateway, "Test Bound API Gateway", "Other Test Listener"), - }), + &structs.TCPRouteConfigEntry{ + Name: "TCP Route", + Kind: structs.TCPRoute, + Parents: []structs.ResourceReference{ + { + Name: "Gateway", + Kind: structs.APIGateway, + SectionName: "Listener 2", + }, + }, + }, }, expectedBoundAPIGateways: []*structs.BoundAPIGatewayConfigEntry{ - makeGateway("Test Bound API Gateway", []structs.BoundAPIGatewayListener{ - makeListener("Test Listener", []structs.ResourceReference{}), - makeListener("Other Test Listener", []structs.ResourceReference{ - makeRef(structs.TCPRoute, "Test TCP Route", ""), - }), - }), + { + 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 Routes bind to each gateway": { - gateways: []*structs.BoundAPIGatewayConfigEntry{ - makeGateway("Test Bound API Gateway", []structs.BoundAPIGatewayListener{ - makeListener("Test Listener", []structs.ResourceReference{}), - }), - makeGateway("Other Test Bound API Gateway", []structs.BoundAPIGatewayListener{ - makeListener("Other Test Listener", []structs.ResourceReference{}), - }), + "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{ - makeRoute(structs.TCPRoute, "Test TCP Route", []structs.ResourceReference{ - makeRef(structs.APIGateway, "Test Bound API Gateway", "Test Listener"), - }), - makeRoute(structs.TCPRoute, "Other Test TCP Route", []structs.ResourceReference{ - makeRef(structs.APIGateway, "Other Test Bound API Gateway", "Other Test Listener"), - }), + &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{ - makeGateway("Test Bound API Gateway", []structs.BoundAPIGatewayListener{ - makeListener("Test Listener", []structs.ResourceReference{ - makeRef(structs.TCPRoute, "Test TCP Route", ""), - }), - }), - makeGateway("Other Test Bound API Gateway", []structs.BoundAPIGatewayListener{ - makeListener("Other Test Listener", []structs.ResourceReference{ - makeRef(structs.TCPRoute, "Other Test TCP Route", ""), - }), - }), + { + 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 { @@ -267,39 +898,3 @@ func TestBindRoutesToGateways(t *testing.T) { }) } } - -func makeRef(kind, name, sectionName string) structs.ResourceReference { - return structs.ResourceReference{ - Kind: kind, - Name: name, - SectionName: sectionName, - } -} - -func makeRoute(kind, name string, parents []structs.ResourceReference) structs.BoundRoute { - switch kind { - case structs.TCPRoute: - return &structs.TCPRouteConfigEntry{ - Kind: structs.TCPRoute, - Name: name, - Parents: parents, - } - default: - panic("unknown route kind") - } -} - -func makeListener(name string, routes []structs.ResourceReference) structs.BoundAPIGatewayListener { - return structs.BoundAPIGatewayListener{ - Name: name, - Routes: routes, - } -} - -func makeGateway(name string, listeners []structs.BoundAPIGatewayListener) *structs.BoundAPIGatewayConfigEntry { - return &structs.BoundAPIGatewayConfigEntry{ - Kind: structs.BoundAPIGateway, - Name: name, - Listeners: listeners, - } -} diff --git a/agent/consul/gateways/gateway_meta.go b/agent/consul/gateways/gateway_meta.go new file mode 100644 index 000000000..b5ea90ca8 --- /dev/null +++ b/agent/consul/gateways/gateway_meta.go @@ -0,0 +1,139 @@ +package gateways + +import ( + "fmt" + + "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 +} + +// 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 { + // Unbind to handle any stale route references. + didUnbind := listener.UnbindRoute(route) + 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() { + i, boundListener := g.boundListenerByName(listener.Name) + if boundListener != nil && boundListener.BindRoute(route) { + 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.BoundRoute) 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 +} diff --git a/agent/consul/gateways/gateway_meta_test.go b/agent/consul/gateways/gateway_meta_test.go new file mode 100644 index 000000000..863ccd08e --- /dev/null +++ b/agent/consul/gateways/gateway_meta_test.go @@ -0,0 +1,373 @@ +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) { + actualDidUnbind := tc.gateway.unbindRoute(tc.route) + + 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 00e4957cd..7f6c94c56 100644 --- a/agent/structs/config_entry_gateways.go +++ b/agent/structs/config_entry_gateways.go @@ -980,77 +980,6 @@ func (e *BoundAPIGatewayConfigEntry) GetEnterpriseMeta() *acl.EnterpriseMeta { return &e.EnterpriseMeta } -func (e *BoundAPIGatewayConfigEntry) UpdateRouteBinding(refs []ResourceReference, route BoundRoute) (bool, map[ResourceReference]error) { - didUpdate := false - errors := make(map[ResourceReference]error) - - if len(e.Listeners) == 0 { - for _, ref := range refs { - errors[ref] = fmt.Errorf("route cannot bind because gateway has no listeners") - } - return false, errors - } - - for i, listener := range e.Listeners { - // Unbind to handle any stale route references. - didUnbind := listener.UnbindRoute(route) - if didUnbind { - didUpdate = true - } - e.Listeners[i] = listener - - for _, ref := range refs { - didBind, err := e.BindRoute(ref, route) - if err != nil { - errors[ref] = err - } - if didBind { - didUpdate = true - } - } - } - - return didUpdate, errors -} - -func (e *BoundAPIGatewayConfigEntry) BindRoute(ref ResourceReference, route BoundRoute) (bool, error) { - if ref.Kind != APIGateway || e.Name != ref.Name || !e.EnterpriseMeta.IsSame(&ref.EnterpriseMeta) { - return false, nil - } - - if len(e.Listeners) == 0 { - return false, fmt.Errorf("route cannot bind because gateway has no listeners") - } - - didBind := false - for i, listener := range e.Listeners { - if listener.Name == ref.SectionName || ref.SectionName == "" { - if listener.BindRoute(route) { - didBind = true - e.Listeners[i] = listener - } - } - } - - if !didBind { - return false, fmt.Errorf("invalid section name: %s", ref.SectionName) - } - - return true, nil -} - -func (e *BoundAPIGatewayConfigEntry) UnbindRoute(route BoundRoute) bool { - didUnbind := false - for i, listener := range e.Listeners { - if listener.UnbindRoute(route) { - didUnbind = true - e.Listeners[i] = listener - } - } - - return didUnbind -} - // BoundAPIGatewayListener is an API gateway listener with information // about the routes and certificates that have successfully bound to it. type BoundAPIGatewayListener struct { @@ -1061,13 +990,14 @@ type BoundAPIGatewayListener struct { // BindRoute is used to create or update a route on the listener. // It returns true if the route was able to be bound to the listener. +// Routes should only bind to listeners with their same section name +// and protocol. Be sure to check both of these before attempting +// to bind a route to the listener. func (l *BoundAPIGatewayListener) BindRoute(route BoundRoute) bool { if l == nil { return false } - // TODO (t-eckert): Add a check that the listener has the same `protocol` as the route. Fail to bind if the protocols do not match. - // Convert the abstract route interface to a ResourceReference. routeRef := ResourceReference{ Kind: route.GetKind(), diff --git a/agent/structs/config_entry_gateways_test.go b/agent/structs/config_entry_gateways_test.go index 75b36e4eb..af1854d96 100644 --- a/agent/structs/config_entry_gateways_test.go +++ b/agent/structs/config_entry_gateways_test.go @@ -1,7 +1,6 @@ package structs import ( - "fmt" "testing" "github.com/stretchr/testify/require" @@ -1343,289 +1342,6 @@ func TestBoundAPIGateway(t *testing.T) { testConfigEntryNormalizeAndValidate(t, cases) } -func TestBoundAPIGatewayBindRoute(t *testing.T) { - cases := map[string]struct { - gateway BoundAPIGatewayConfigEntry - route BoundRoute - expectedGateway BoundAPIGatewayConfigEntry - expectedDidBind bool - expectedErr error - }{ - "Bind TCP Route to Gateway": { - gateway: BoundAPIGatewayConfigEntry{ - Kind: BoundAPIGateway, - Name: "Test Bound API Gateway", - Listeners: []BoundAPIGatewayListener{ - { - Name: "Test Listener", - Routes: []ResourceReference{}, - }, - }, - }, - route: &TCPRouteConfigEntry{ - Kind: TCPRoute, - Name: "Test Route", - Parents: []ResourceReference{ - { - Kind: APIGateway, - Name: "Test Bound API Gateway", - SectionName: "Test Listener", - }, - }, - }, - expectedGateway: BoundAPIGatewayConfigEntry{ - Kind: BoundAPIGateway, - Name: "Test Bound API Gateway", - Listeners: []BoundAPIGatewayListener{ - { - Name: "Test Listener", - Routes: []ResourceReference{ - { - Kind: TCPRoute, - Name: "Test Route", - }, - }, - }, - }, - }, - expectedDidBind: true, - }, - "Bind TCP Route with wildcard section name to all listeners on Gateway": { - gateway: BoundAPIGatewayConfigEntry{ - Kind: BoundAPIGateway, - Name: "Test Bound API Gateway", - Listeners: []BoundAPIGatewayListener{ - { - Name: "Test Listener 1", - Routes: []ResourceReference{}, - }, - { - Name: "Test Listener 2", - Routes: []ResourceReference{}, - }, - { - Name: "Test Listener 3", - Routes: []ResourceReference{}, - }, - }, - }, - route: &TCPRouteConfigEntry{ - Kind: TCPRoute, - Name: "Test Route", - Parents: []ResourceReference{ - { - Kind: APIGateway, - Name: "Test Bound API Gateway", - }, - }, - }, - expectedGateway: BoundAPIGatewayConfigEntry{ - Kind: BoundAPIGateway, - Name: "Test Bound API Gateway", - Listeners: []BoundAPIGatewayListener{ - { - Name: "Test Listener 1", - Routes: []ResourceReference{ - { - Kind: TCPRoute, - Name: "Test Route", - }, - }, - }, - { - Name: "Test Listener 2", - Routes: []ResourceReference{ - { - Kind: TCPRoute, - Name: "Test Route", - }, - }, - }, - { - Name: "Test Listener 3", - Routes: []ResourceReference{ - { - Kind: TCPRoute, - Name: "Test Route", - }, - }, - }, - }, - }, - expectedDidBind: true, - }, - "TCP Route cannot bind to Gateway because the parent reference kind is not APIGateway": { - gateway: BoundAPIGatewayConfigEntry{ - Kind: BoundAPIGateway, - Name: "Test Bound API Gateway", - Listeners: []BoundAPIGatewayListener{}, - }, - route: &TCPRouteConfigEntry{ - Kind: TCPRoute, - Name: "Test Route", - Parents: []ResourceReference{ - { - Name: "Test Bound API Gateway", - SectionName: "Test Listener", - }, - }, - }, - expectedGateway: BoundAPIGatewayConfigEntry{ - Kind: TerminatingGateway, - Name: "Test Bound API Gateway", - Listeners: []BoundAPIGatewayListener{}, - }, - expectedDidBind: false, - expectedErr: nil, - }, - "TCP Route cannot bind to Gateway because the parent reference name does not match": { - gateway: BoundAPIGatewayConfigEntry{ - Kind: BoundAPIGateway, - Name: "Test Bound API Gateway", - Listeners: []BoundAPIGatewayListener{}, - }, - route: &TCPRouteConfigEntry{ - Kind: TCPRoute, - Name: "Test Route", - Parents: []ResourceReference{ - { - Kind: APIGateway, - Name: "Other Test Bound API Gateway", - SectionName: "Test Listener", - }, - }, - }, - expectedGateway: BoundAPIGatewayConfigEntry{ - Kind: BoundAPIGateway, - Name: "Test Bound API Gateway", - Listeners: []BoundAPIGatewayListener{}, - }, - expectedDidBind: false, - expectedErr: nil, - }, - "TCP Route cannot bind to Gateway because it lacks listeners": { - gateway: BoundAPIGatewayConfigEntry{ - Kind: BoundAPIGateway, - Name: "Test Bound API Gateway", - Listeners: []BoundAPIGatewayListener{}, - }, - route: &TCPRouteConfigEntry{ - Kind: TCPRoute, - Name: "Test Route", - Parents: []ResourceReference{ - { - Kind: APIGateway, - Name: "Test Bound API Gateway", - SectionName: "Test Listener", - }, - }, - }, - expectedGateway: BoundAPIGatewayConfigEntry{ - Kind: BoundAPIGateway, - Name: "Test Bound API Gateway", - Listeners: []BoundAPIGatewayListener{}, - }, - expectedDidBind: false, - expectedErr: fmt.Errorf("route cannot bind because gateway has no listeners"), - }, - "TCP Route cannot bind to Gateway because it has an invalid section name": { - gateway: BoundAPIGatewayConfigEntry{ - Kind: BoundAPIGateway, - Name: "Test Bound API Gateway", - Listeners: []BoundAPIGatewayListener{}, - }, - route: &TCPRouteConfigEntry{ - Kind: TCPRoute, - Name: "Test Route", - Parents: []ResourceReference{ - { - Kind: APIGateway, - Name: "Test Bound API Gateway", - SectionName: "Other Test Listener", - }, - }, - }, - expectedGateway: BoundAPIGatewayConfigEntry{ - Kind: BoundAPIGateway, - Name: "Test Bound API Gateway", - Listeners: []BoundAPIGatewayListener{}, - }, - expectedDidBind: false, - expectedErr: fmt.Errorf("route cannot bind because gateway has no listeners"), - }, - } - - for name, tc := range cases { - t.Run(name, func(t *testing.T) { - ref := tc.route.GetParents()[0] - - actualDidBind, actualErr := tc.gateway.BindRoute(ref, tc.route) - - require.Equal(t, tc.expectedDidBind, actualDidBind) - require.Equal(t, tc.expectedErr, actualErr) - require.Equal(t, tc.expectedGateway.Listeners, tc.gateway.Listeners) - }) - } -} - -func TestBoundAPIGatewayUnbindRoute(t *testing.T) { - cases := map[string]struct { - gateway BoundAPIGatewayConfigEntry - route BoundRoute - expectedGateway BoundAPIGatewayConfigEntry - expectedDidUnbind bool - }{ - "TCP Route unbinds from Gateway": { - gateway: BoundAPIGatewayConfigEntry{ - Kind: BoundAPIGateway, - Name: "Test Bound API Gateway", - Listeners: []BoundAPIGatewayListener{ - { - Name: "Test Listener", - Routes: []ResourceReference{ - { - Kind: TCPRoute, - Name: "Test Route", - }, - }, - }, - }, - }, - route: &TCPRouteConfigEntry{ - Kind: TCPRoute, - Name: "Test Route", - Parents: []ResourceReference{ - { - Kind: BoundAPIGateway, - Name: "Other Test Bound API Gateway", - SectionName: "Test Listener", - }, - }, - }, - expectedGateway: BoundAPIGatewayConfigEntry{ - Kind: BoundAPIGateway, - Name: "Test Bound API Gateway", - Listeners: []BoundAPIGatewayListener{ - { - Name: "Test Listener", - Routes: []ResourceReference{}, - }, - }, - }, - expectedDidUnbind: true, - }, - } - - for name, tc := range cases { - t.Run(name, func(t *testing.T) { - actualDidUnbind := tc.gateway.UnbindRoute(tc.route) - - require.Equal(t, tc.expectedDidUnbind, actualDidUnbind) - require.Equal(t, tc.expectedGateway.Listeners, tc.gateway.Listeners) - }) - } -} - func TestListenerBindRoute(t *testing.T) { cases := map[string]struct { listener BoundAPIGatewayListener diff --git a/agent/structs/config_entry_routes.go b/agent/structs/config_entry_routes.go index cd82b795d..a457b772b 100644 --- a/agent/structs/config_entry_routes.go +++ b/agent/structs/config_entry_routes.go @@ -11,6 +11,7 @@ import ( type BoundRoute interface { ConfigEntry GetParents() []ResourceReference + GetProtocol() APIGatewayListenerProtocol } // HTTPRouteConfigEntry manages the configuration for a HTTP route @@ -39,6 +40,18 @@ func (e *HTTPRouteConfigEntry) GetName() string { return e.Name } +func (e *HTTPRouteConfigEntry) GetParents() []ResourceReference { + if e == nil { + return []ResourceReference{} + } + // TODO HTTP Route should have "parents". Andrew will implement this in his work. + return []ResourceReference{} +} + +func (e *HTTPRouteConfigEntry) GetProtocol() APIGatewayListenerProtocol { + return ListenerProtocolHTTP +} + func (e *HTTPRouteConfigEntry) Normalize() error { return nil } @@ -115,6 +128,17 @@ func (e *TCPRouteConfigEntry) GetName() string { return e.Name } +func (e *TCPRouteConfigEntry) GetParents() []ResourceReference { + if e == nil { + return []ResourceReference{} + } + return e.Parents +} + +func (e *TCPRouteConfigEntry) GetProtocol() APIGatewayListenerProtocol { + return ListenerProtocolTCP +} + func (e *TCPRouteConfigEntry) GetMeta() map[string]string { if e == nil { return nil @@ -160,13 +184,6 @@ func (e *TCPRouteConfigEntry) CanWrite(authz acl.Authorizer) error { return authz.ToAllowAuthorizer().MeshWriteAllowed(&authzContext) } -func (e *TCPRouteConfigEntry) GetParents() []ResourceReference { - if e == nil { - return []ResourceReference{} - } - return e.Parents -} - func (e *TCPRouteConfigEntry) GetRaftIndex() *RaftIndex { if e == nil { return &RaftIndex{}