0fafa010bc
Add CE version of Gateway Upstream Disambiguation Co-authored-by: Thomas Eckert <teckert@hashicorp.com>
307 lines
10 KiB
Go
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
|
|
}
|