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