open-consul/agent/consul/discoverychain/gateway.go
Andrew Stucki 0fafa010bc
[Backport 1.16.x] Add CE version of Gateway Upstream Disambiguation (#19867)
Add CE version of Gateway Upstream Disambiguation

Co-authored-by: Thomas Eckert <teckert@hashicorp.com>
2023-12-08 11:46:23 -05:00

307 lines
10 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package discoverychain
import (
"fmt"
"hash/crc32"
"sort"
"strconv"
"strings"
"github.com/hashicorp/consul/agent/configentry"
"github.com/hashicorp/consul/agent/structs"
)
// GatewayChainSynthesizer is used to synthesize a discovery chain for a
// gateway from its configuration and multiple other discovery chains.
type GatewayChainSynthesizer struct {
datacenter string
trustDomain string
suffix string
gateway *structs.APIGatewayConfigEntry
hostname string
matchesByHostname map[string][]hostnameMatch
tcpRoutes []structs.TCPRouteConfigEntry
}
type hostnameMatch struct {
match structs.HTTPMatch
filters structs.HTTPFilters
services []structs.HTTPService
}
// NewGatewayChainSynthesizer creates a new GatewayChainSynthesizer for the
// given gateway and datacenter.
func NewGatewayChainSynthesizer(datacenter, trustDomain, suffix string, gateway *structs.APIGatewayConfigEntry) *GatewayChainSynthesizer {
return &GatewayChainSynthesizer{
datacenter: datacenter,
trustDomain: trustDomain,
suffix: suffix,
gateway: gateway,
matchesByHostname: map[string][]hostnameMatch{},
}
}
// AddTCPRoute adds a TCPRoute to use in synthesizing a discovery chain
func (l *GatewayChainSynthesizer) AddTCPRoute(route structs.TCPRouteConfigEntry) {
l.tcpRoutes = append(l.tcpRoutes, route)
}
// SetHostname sets the base hostname for a listener that this is being synthesized for
func (l *GatewayChainSynthesizer) SetHostname(hostname string) {
l.hostname = hostname
}
// AddHTTPRoute takes a new route and flattens its rule matches out per hostname.
// This is required since a single route can specify multiple hostnames, and a
// single hostname can be specified in multiple routes. Routing for a given
// hostname must behave based on the aggregate of all rules that apply to it.
func (l *GatewayChainSynthesizer) AddHTTPRoute(route structs.HTTPRouteConfigEntry) {
l.matchesByHostname = initHostMatches(l.hostname, &route, l.matchesByHostname)
}
func initHostMatches(hostname string, route *structs.HTTPRouteConfigEntry, currentMatches map[string][]hostnameMatch) map[string][]hostnameMatch {
hostnames := route.FilteredHostnames(hostname)
for _, host := range hostnames {
matches, ok := currentMatches[host]
if !ok {
matches = []hostnameMatch{}
}
for _, rule := range route.Rules {
// If a rule has no matches defined, add default match
if rule.Matches == nil {
rule.Matches = []structs.HTTPMatch{}
}
if len(rule.Matches) == 0 {
rule.Matches = []structs.HTTPMatch{{
Path: structs.HTTPPathMatch{
Match: structs.HTTPPathMatchPrefix,
Value: "/",
},
}}
}
// Add all matches for this rule to the list for this hostname
for _, match := range rule.Matches {
matches = append(matches, hostnameMatch{
match: match,
filters: rule.Filters,
services: rule.Services,
})
}
}
currentMatches[host] = matches
}
return currentMatches
}
// Synthesize assembles a synthetic discovery chain from multiple other discovery chains
// that have StartNodes that are referenced by routers or splitters in the entries for the
// given CompileRequest.
//
// This is currently used to help API gateways masquarade as ingress gateways
// by providing a set of virtual config entries that change the routing behavior
// to upstreams referenced in the given HTTPRoutes or TCPRoutes.
func (l *GatewayChainSynthesizer) Synthesize(chains ...*structs.CompiledDiscoveryChain) ([]structs.IngressService, []*structs.CompiledDiscoveryChain, error) {
if len(chains) == 0 {
return nil, nil, fmt.Errorf("must provide at least one compiled discovery chain")
}
services, set := l.synthesizeEntries()
if len(set) == 0 {
// we can't actually compile a discovery chain, i.e. we're using a TCPRoute-based listener, instead, just return the ingresses
// and the pre-compiled discovery chains
return services, chains, nil
}
compiledChains := make([]*structs.CompiledDiscoveryChain, 0, len(set))
for i, service := range services {
entries := set[i]
compiled, err := Compile(CompileRequest{
ServiceName: service.Name,
EvaluateInNamespace: service.NamespaceOrDefault(),
EvaluateInPartition: service.PartitionOrDefault(),
EvaluateInDatacenter: l.datacenter,
EvaluateInTrustDomain: l.trustDomain,
Entries: entries,
})
if err != nil {
return nil, nil, err
}
node := compiled.Nodes[compiled.StartNode]
if node.IsRouter() {
resolverPrefix := structs.DiscoveryGraphNodeTypeResolver + ":" + node.Name
// clean out the clusters that will get added for the router
for name := range compiled.Nodes {
if strings.HasPrefix(name, resolverPrefix) {
delete(compiled.Nodes, name)
}
}
// clean out the route rules that'll get added for the router
filtered := []*structs.DiscoveryRoute{}
for _, route := range node.Routes {
if strings.HasPrefix(route.NextNode, resolverPrefix) {
continue
}
filtered = append(filtered, route)
}
node.Routes = filtered
}
compiled.Nodes[compiled.StartNode] = node
// fix up the nodes for the terminal targets to either be a splitter or resolver if there is no splitter present
for name, node := range compiled.Nodes {
switch node.Type {
// we should only have these two types
case structs.DiscoveryGraphNodeTypeRouter:
for i, route := range node.Routes {
node.Routes[i].NextNode = targetForResolverNode(route.NextNode, chains)
}
case structs.DiscoveryGraphNodeTypeSplitter:
for i, split := range node.Splits {
node.Splits[i].NextNode = targetForResolverNode(split.NextNode, chains)
}
}
compiled.Nodes[name] = node
}
for _, c := range chains {
for id, target := range c.Targets {
compiled.Targets[id] = target
}
for id, node := range c.Nodes {
compiled.Nodes[id] = node
}
compiled.EnvoyExtensions = append(compiled.EnvoyExtensions, c.EnvoyExtensions...)
}
compiledChains = append(compiledChains, compiled)
}
return services, compiledChains, nil
}
// consolidateHTTPRoutes combines all rules into the shortest possible list of routes
// with one route per hostname containing all rules for that hostname.
func (l *GatewayChainSynthesizer) consolidateHTTPRoutes() []structs.HTTPRouteConfigEntry {
return consolidateHTTPRoutes(l.matchesByHostname, l.suffix, l.gateway)
}
// ConsolidateHTTPRoutes takes in one or more HTTPRoutes and consolidates them down to the minimum
// set of HTTPRoutes that can represent the same set of rules. This should result in approx. one
// HTTPRoute per hostname.
func ConsolidateHTTPRoutes(gateway *structs.APIGatewayConfigEntry, listener *structs.APIGatewayListener, routes ...*structs.HTTPRouteConfigEntry) []structs.HTTPRouteConfigEntry {
matches := map[string][]hostnameMatch{}
for _, route := range routes {
matches = initHostMatches(listener.GetHostname(), route, matches)
}
return consolidateHTTPRoutes(matches, listener.Name, gateway)
}
// consolidateHTTPRoutes combines all rules into the shortest possible list of routes
// with one route per hostname containing all rules for that hostname.
func consolidateHTTPRoutes(matchesByHostname map[string][]hostnameMatch, listenerName string, gateway *structs.APIGatewayConfigEntry) []structs.HTTPRouteConfigEntry {
var routes []structs.HTTPRouteConfigEntry
for hostname, rules := range matchesByHostname {
// Create route for this hostname
route := structs.HTTPRouteConfigEntry{
Kind: structs.HTTPRoute,
Name: fmt.Sprintf("%s-%s-%s", gateway.Name, listenerName, hostsKey(hostname)),
Hostnames: []string{hostname},
Rules: make([]structs.HTTPRouteRule, 0, len(rules)),
Meta: gateway.Meta,
EnterpriseMeta: gateway.EnterpriseMeta,
}
// Sort rules for this hostname in order of precedence
sort.SliceStable(rules, func(i, j int) bool {
return compareHTTPRules(rules[i].match, rules[j].match)
})
// Add all rules for this hostname
for _, rule := range rules {
route.Rules = append(route.Rules, structs.HTTPRouteRule{
Matches: []structs.HTTPMatch{rule.match},
Filters: rule.filters,
Services: rule.services,
})
}
routes = append(routes, route)
}
return routes
}
func targetForResolverNode(nodeName string, chains []*structs.CompiledDiscoveryChain) string {
resolverPrefix := structs.DiscoveryGraphNodeTypeResolver + ":"
splitterPrefix := structs.DiscoveryGraphNodeTypeSplitter + ":"
if !strings.HasPrefix(nodeName, resolverPrefix) {
return nodeName
}
splitterName := splitterPrefix + strings.TrimPrefix(nodeName, resolverPrefix)
for _, c := range chains {
targetChainPrefix := resolverPrefix + c.ID()
if strings.HasPrefix(nodeName, targetChainPrefix) && len(c.Nodes) == 1 {
// we have a virtual resolver that just maps to another resolver, return
// the given node name
return c.StartNode
}
for name, node := range c.Nodes {
if node.IsSplitter() && strings.HasPrefix(splitterName, name) {
return name
}
}
}
return nodeName
}
func hostsKey(hosts ...string) string {
sort.Strings(hosts)
hostsHash := crc32.NewIEEE()
for _, h := range hosts {
if _, err := hostsHash.Write([]byte(h)); err != nil {
continue
}
}
return strconv.FormatUint(uint64(hostsHash.Sum32()), 16)
}
func (l *GatewayChainSynthesizer) synthesizeEntries() ([]structs.IngressService, []*configentry.DiscoveryChainSet) {
services := []structs.IngressService{}
entries := []*configentry.DiscoveryChainSet{}
for _, route := range l.consolidateHTTPRoutes() {
ingress, router, splitters, defaults := synthesizeHTTPRouteDiscoveryChain(route)
services = append(services, ingress)
entrySet := configentry.NewDiscoveryChainSet()
entrySet.AddRouters(router)
entrySet.AddSplitters(splitters...)
entrySet.AddServices(defaults...)
entries = append(entries, entrySet)
}
for _, route := range l.tcpRoutes {
services = append(services, synthesizeTCPRouteDiscoveryChain(route)...)
}
return services, entries
}